ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
lib/network/websocket/server.c
Go to the documentation of this file.
1
11#include <ascii-chat/version.h>
13#include <time.h>
14#include <yyjson.h>
16#include <ascii-chat/log/log.h>
18#include <ascii-chat/common.h>
28#include <libwebsockets.h>
29#include <string.h>
30#include <errno.h>
31#include <openssl/err.h>
32#ifndef _WIN32
33#include <sys/socket.h>
34#include <netinet/tcp.h>
35#include <netinet/in.h>
36#include <unistd.h>
37#ifndef R_OK
38#define R_OK 4
39#endif
40#endif
41
42// Shared internal types (websocket_recv_msg_t, websocket_transport_data_t)
45
64
65// Per-connection callback counters for diagnosing event loop interleaving.
66// These track how many RECEIVE vs WRITEABLE callbacks fire during message assembly.
67// TODO: Make these per-connection instead of global for proper multi-client support
68static atomic_t g_receive_callback_count = {0};
69static atomic_t g_writeable_callback_count = {0};
70
71// Forward declaration for websocket_protocols (defined later in file)
72static struct lws_protocols websocket_protocols[];
73
79static int websocket_server_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in,
80 size_t len) {
82 const char *proto_name = lws_get_protocol(wsi) ? lws_get_protocol(wsi)->name : "NULL";
83
84 // LOG EVERY SINGLE CALLBACK WITH PROTOCOL NAME
85 log_dev("🔵 CALLBACK: reason=%d, proto=%s, wsi=%p, len=%zu", reason, proto_name, (void *)wsi, len);
86
87 switch (reason) {
88 case LWS_CALLBACK_ESTABLISHED: {
89 // New WebSocket connection established
90 uint64_t established_ns = time_get_ns();
91 log_info("🔵🔵🔵 LWS_CALLBACK_ESTABLISHED FIRED! wsi=%p", (void *)wsi);
92 log_info("[LWS_CALLBACK_ESTABLISHED] WebSocket client connection established at timestamp=%llu",
93 (unsigned long long)established_ns);
94 log_info("WebSocket client connected");
95
96 // Get server instance from protocol user data
97 const struct lws_protocols *protocol = lws_get_protocol(wsi);
98 log_debug("[LWS_CALLBACK_ESTABLISHED] Got protocol: %p", (void *)protocol);
99 if (!protocol || !protocol->user) {
100 log_error("[LWS_CALLBACK_ESTABLISHED] FAILED: Missing protocol user data (protocol=%p, user=%p)",
101 (void *)protocol, protocol ? protocol->user : NULL);
102 return -1;
103 }
104 websocket_server_t *server = (websocket_server_t *)protocol->user;
105 log_debug("[LWS_CALLBACK_ESTABLISHED] Got server: %p, handler: %p", (void *)server, (void *)server->handler);
106
107 // Initialize connection data
108 conn_data->server = server;
109 conn_data->transport = NULL;
110 conn_data->handler_started = false;
111 conn_data->pending_send_data = NULL;
112 conn_data->pending_send_len = 0;
113 conn_data->pending_send_offset = 0;
114 conn_data->has_pending_send = false;
115
116 // Get client address
117 char client_ip[64] = {0};
118 if (!lws_get_peer_simple(wsi, client_ip, sizeof(client_ip)) || client_ip[0] == '\0') {
119 safe_snprintf(client_ip, sizeof(client_ip), "unknown");
120 log_warn("Could not determine WebSocket client IP address");
121 }
122
123 log_info("WebSocket client connected from %s", client_ip);
124 log_debug("[LWS_CALLBACK_ESTABLISHED] Client IP: %s", client_ip);
125
126 // Optimize TCP for high-throughput large message transfer.
127 // TCP delayed ACK (default ~40ms) causes the sender to stall waiting for ACKs,
128 // creating ~30ms gaps between 128KB chunk deliveries for large messages.
129 // TCP_QUICKACK forces immediate ACKs so the sender can push data continuously (Linux only).
130 // TCP_NODELAY disables Nagle's algorithm for the send path.
131#ifdef __linux__
132 {
133 int fd = lws_get_socket_fd(wsi);
134 if (fd >= 0) {
135 int quickack = 1;
136 if (setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &quickack, sizeof(quickack)) < 0) {
137 log_warn("Failed to set TCP_QUICKACK: %s", SAFE_STRERROR(errno));
138 }
139 int nodelay = 1;
140 if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)) < 0) {
141 log_warn("Failed to set TCP_NODELAY: %s", SAFE_STRERROR(errno));
142 }
143 // Do NOT set SO_RCVBUF/SO_SNDBUF manually.
144 // Setting SO_RCVBUF disables TCP autotuning on Linux, which locks the receive buffer
145 // at the rmem_default (212KB). Without manual override, the kernel can autotune up to
146 // tcp_rmem max (typically 6MB), allowing the entire 921KB video frame to fit in one
147 // TCP window without flow control stalls.
148 int actual_rcv = 0, actual_snd = 0;
149 socklen_t optlen = sizeof(int);
150 getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &actual_rcv, &optlen);
151 getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &actual_snd, &optlen);
152 log_info("[WS_SOCKET] TCP_QUICKACK=1, TCP_NODELAY=1, SO_RCVBUF=%d (autotuned), SO_SNDBUF=%d", actual_rcv,
153 actual_snd);
154 }
155 }
156#endif
157
158 // Create ACIP WebSocket server transport for this connection
159 // Note: We pass NULL for crypto_ctx here - crypto handshake happens at ACIP level
160 log_debug("[LWS_CALLBACK_ESTABLISHED] Creating ACIP WebSocket transport...");
161 char ws_transport_name[64];
162 snprintf(ws_transport_name, sizeof(ws_transport_name), "server_%p", (void *)wsi);
163 conn_data->transport = acip_websocket_server_transport_create(ws_transport_name, wsi, NULL);
164 if (!conn_data->transport) {
165 log_error("[LWS_CALLBACK_ESTABLISHED] FAILED: acip_websocket_server_transport_create returned NULL");
166 return -1;
167 }
168 log_debug("[LWS_CALLBACK_ESTABLISHED] Transport created: %p", (void *)conn_data->transport);
169
170 // Create client context for handler
171 websocket_client_context_t *client_ctx =
173 if (!client_ctx) {
174 log_error("Failed to allocate client context");
176 conn_data->transport = NULL;
177 return -1;
178 }
179
180 client_ctx->transport = conn_data->transport;
181 SAFE_STRNCPY(client_ctx->client_ip, client_ip, sizeof(client_ctx->client_ip));
182 client_ctx->client_port = 0; // WebSocket doesn't expose client port easily
183 client_ctx->user_data = server->user_data;
184 client_ctx->is_secure = lws_is_ssl(wsi); // Check if connection is TLS (wss://)
185 client_ctx->auth_required = false; // Set by server handler if authentication is required
186
187 // Queue handler to thread pool (no pthread_create from callback context)
188 // The handler_pool was created at server startup with pre-allocated workers
189 log_info("🔵 ABOUT TO QUEUE HANDLER: handler=%p, ctx=%p", (void *)server->handler, (void *)client_ctx);
190 log_debug("[LWS_CALLBACK_ESTABLISHED] Queueing handler to work pool (handler=%p, ctx=%p)...",
191 (void *)server->handler, (void *)client_ctx);
192
193 if (!server->handler_pool) {
194 log_error("[LWS_CALLBACK_ESTABLISHED] FAILED: handler_pool is NULL");
195 SAFE_FREE(client_ctx);
197 conn_data->transport = NULL;
198 return -1;
199 }
200
201 // Mark the handler as owning the transport before it becomes visible to a
202 // worker. A peer can close immediately after the upgrade; CLOSED must not
203 // destroy a transport that has already been queued for handling.
204 conn_data->handler_started = true;
205 asciichat_error_t queue_result =
206 thread_pool_queue_work("websocket_handler_established", server->handler_pool, server->handler, client_ctx);
207 log_info("🔵 thread_pool_queue_work returned: %s", queue_result == ASCIICHAT_OK ? "OK" : "ERROR");
208
209 if (queue_result != ASCIICHAT_OK) {
210 log_error("[LWS_CALLBACK_ESTABLISHED] FAILED: thread_pool_queue_work returned error");
211 conn_data->handler_started = false;
212 SAFE_FREE(client_ctx);
214 conn_data->transport = NULL;
215 return -1;
216 }
217
218 log_debug("[LWS_CALLBACK_ESTABLISHED] Handler work queued successfully");
219 log_info("★★★ ESTABLISHED CALLBACK SUCCESS - handler work queued! ★★★");
220 break;
221 }
222
223 case LWS_CALLBACK_CLOSED: {
224 // Connection closed
225 uint64_t close_callback_start_ns = time_get_ns();
226 log_info("★★★ LWS_CALLBACK_CLOSED FIRED - WHY IS CONNECTION CLOSING? ★★★");
227 log_info("[LWS_CALLBACK_CLOSED] WebSocket client disconnected, wsi=%p, handler_started=%d, timestamp=%llu",
228 (void *)wsi, conn_data ? conn_data->handler_started : -1, (unsigned long long)close_callback_start_ns);
229
230 // Try to extract close code from 'in' parameter if available
231 uint16_t close_code = 0;
232 if (in && len >= 2) {
233 close_code = (uint16_t)((uint8_t *)in)[0] << 8 | ((uint8_t *)in)[1];
234 log_info("[LWS_CALLBACK_CLOSED] Close frame received with code=%u (1000=normal, 1001=going away, 1006=abnormal, "
235 "1009=message too big)",
236 close_code);
237 } else {
238 log_info(
239 "[LWS_CALLBACK_CLOSED] No close frame (in=%p, len=%zu) - connection closed without WebSocket close handshake",
240 in, len);
241 }
242
243 // Mark cleanup in progress to prevent race conditions with other threads accessing transport
244 if (conn_data) {
245 conn_data->cleaning_up = true;
246 }
247
248 // Clean up pending send data only if not already freed
249 if (conn_data && conn_data->has_pending_send && conn_data->pending_send_data) {
250 SAFE_FREE(conn_data->pending_send_data);
251 conn_data->pending_send_data = NULL;
252 conn_data->has_pending_send = false;
253 }
254
255 // Close the transport to mark it as disconnected.
256 // This signals the receive thread to exit, allowing clean shutdown.
257 // Guard against use-after-free: check both conn_data and transport are valid
258 acip_transport_t *transport_snapshot = NULL;
259 if (conn_data && conn_data->transport) {
260 transport_snapshot = conn_data->transport;
261 conn_data->transport = NULL; // NULL out immediately to prevent race condition
262
263 log_debug("[LWS_CALLBACK_CLOSED] Closing transport=%p for wsi=%p", (void *)transport_snapshot, (void *)wsi);
264 // Now safely close the transport (methods pointer is stable)
265 if (transport_snapshot && transport_snapshot->methods) {
266 transport_snapshot->methods->close(transport_snapshot);
267 }
268 }
269
270 if (conn_data && conn_data->handler_started) {
271 // Handler is managed by thread pool - no join needed
272 // The pool worker will finish handling the transport and return to the pool
273 // Transport was already closed above, signaling the handler to exit
274 // DO NOT destroy the transport here - the handler might still be using it!
275 // The handler will free the context, and the transport will be cleaned up
276 // through the client structure (remove_client handles transport cleanup)
277 conn_data->handler_started = false;
278 log_debug("[LWS_CALLBACK_CLOSED] Handler was queued to pool (it will cleanup when done)");
279 log_debug("[LWS_CALLBACK_CLOSED] NOT destroying transport here - handler is still executing");
280 transport_snapshot = NULL; // Don't destroy below
281 }
282
283 // Destroy the transport only if the handler is NOT running
284 // (Handler manages cleanup when it's running)
285 if (transport_snapshot) {
286 log_debug("[LWS_CALLBACK_CLOSED] Destroying transport=%p", (void *)transport_snapshot);
287 acip_transport_destroy(transport_snapshot);
288 }
289
290 // Ensure transport pointer is NULL
291 if (conn_data) {
292 conn_data->transport = NULL;
293 }
294
295 uint64_t close_callback_end_ns = time_get_ns();
296 char total_duration_str[32];
297 time_pretty(close_callback_end_ns - close_callback_start_ns, -1, total_duration_str, sizeof(total_duration_str));
298 log_info("[LWS_CALLBACK_CLOSED] Complete cleanup took %s", total_duration_str);
299 break;
300 }
301
302 case LWS_CALLBACK_SERVER_WRITEABLE: {
303 START_TIMER("server_writeable");
304 uint64_t writeable_callback_start_ns = time_get_ns();
305 atomic_fetch_add_u64(&g_writeable_callback_count, 1);
306 log_debug("=== LWS_CALLBACK_SERVER_WRITEABLE FIRED === wsi=%p, timestamp=%llu", (void *)wsi,
307 (unsigned long long)writeable_callback_start_ns);
308
309 // Validate preconditions
310 if (!conn_data) {
311 log_dev_every(4500 * US_PER_MS_INT, "SERVER_WRITEABLE: No conn_data");
312 break;
313 }
314
315 if (conn_data->cleaning_up) {
316 log_dev_every(4500 * US_PER_MS_INT, "SERVER_WRITEABLE: Cleanup in progress, skipping");
317 break;
318 }
319
320 acip_transport_t *transport_snapshot = conn_data->transport;
321 if (!transport_snapshot) {
322 log_dev_every(4500 * US_PER_MS_INT, "SERVER_WRITEABLE: No transport");
323 break;
324 }
325
326 websocket_transport_data_t *ws_data = (websocket_transport_data_t *)transport_snapshot->impl_data;
327 if (!ws_data || !ws_data->send_queue) {
328 log_dev_every(4500 * US_PER_MS_INT, "SERVER_WRITEABLE: No ws_data or send_queue");
329 break;
330 }
331
332 // Dequeue and send messages until pipe is choked or queue is empty
333 // This follows the libwebsockets reference pattern (protocol_lws_mirror.c)
334 websocket_recv_msg_t msg = {0};
335
336 do {
337 // Dequeue one message
338 mutex_lock(&ws_data->send_mutex);
339 if (ringbuffer_is_empty(ws_data->send_queue)) {
340 mutex_unlock(&ws_data->send_mutex);
341 break; // No more messages
342 }
343 if (!ringbuffer_read(ws_data->send_queue, &msg)) {
344 mutex_unlock(&ws_data->send_mutex);
345 break; // Failed to read
346 }
347 mutex_unlock(&ws_data->send_mutex);
348
349 if (!msg.data || msg.len == 0) {
350 log_error("SERVER_WRITEABLE: Dequeued invalid message (data=%p, len=%zu)", (void *)msg.data, msg.len);
351 if (msg.data) {
352 buffer_pool_free(NULL, msg.data, LWS_PRE + msg.len);
353 }
354 continue;
355 }
356
357 // Let libwebsockets handle ALL fragmentation and FIN bit management internally
358 // This avoids manual fragmentation bugs with FIN bit handling
359 // lws_write() with LWS_WRITE_BINARY automatically fragments based on MTU
360 // and properly manages continuation frames and FIN bits
361
362 log_debug(" SERVER sending complete message: %zu bytes", msg.len);
363
364 START_TIMER("lws_write");
365 int written = lws_write(wsi, msg.data + LWS_PRE, msg.len, LWS_WRITE_BINARY);
366 STOP_TIMER_AND_LOG(dev, 0, "lws_write", "[LWS_WRITE]");
367
368 if (written < 0) {
369 log_error("Server WebSocket write error: %d", written);
370 // Connection error
371 buffer_pool_free(NULL, msg.data, LWS_PRE + msg.len);
372 break;
373 }
374
375 size_t total_written = (size_t)written;
376
377 if (total_written < msg.len) {
378 // Partial send - re-queue the unsent portion for the next callback
379 log_debug("Server WebSocket partial message: sent %zu/%zu bytes (re-queueing %zu bytes for next callback)",
380 total_written, msg.len, msg.len - total_written);
381
382 // Create new message for the unsent data
383 websocket_recv_msg_t unsent_msg;
384 unsent_msg.data = msg.data; // Keep the same buffer allocation
385 unsent_msg.len = msg.len - total_written;
386 unsent_msg.first = 0; // No longer first fragment
387 unsent_msg.final = msg.final; // Preserve final flag
388
389 // Re-queue unsent portion
390 mutex_lock(&ws_data->send_mutex);
391 bool requeue_success = ringbuffer_write(ws_data->send_queue, &unsent_msg);
392 if (!requeue_success) {
393 mutex_unlock(&ws_data->send_mutex);
394 log_error("Failed to re-queue unsent data (%zu bytes)", unsent_msg.len);
395 buffer_pool_free(NULL, msg.data, LWS_PRE + msg.len);
396 break;
397 }
398 mutex_unlock(&ws_data->send_mutex);
399
400 // Request immediate callback to continue sending
401 lws_callback_on_writable(wsi);
402 break; // Stop processing more messages, let next callback retry
403 }
404
405 log_debug(">>> lws_write() sent %zu/%zu bytes (fragmented), wsi=%p", total_written, msg.len, (void *)wsi);
406 // Only free buffer when entire message was sent successfully
407 buffer_pool_free(NULL, msg.data, LWS_PRE + msg.len);
408
409 // Continue sending while pipe is not choked and we have data
410 // This matches lws_mirror.c line 396: } while (!lws_send_pipe_choked(wsi));
411 } while (!lws_send_pipe_choked(wsi));
412
413 // If messages still queued and pipe not choked, request callback for next batch
414 mutex_lock(&ws_data->send_mutex);
415 bool has_more = !ringbuffer_is_empty(ws_data->send_queue);
416 mutex_unlock(&ws_data->send_mutex);
417
418 if (has_more && !lws_send_pipe_choked(wsi)) {
419 log_dev_every(4500 * US_PER_MS_INT, "SERVER_WRITEABLE: More messages queued, requesting callback");
420 lws_callback_on_writable(wsi);
421 }
422
423 // Record timing for this callback
424 uint64_t writeable_callback_end_ns = time_get_ns();
426 writeable_callback_end_ns);
427 STOP_TIMER_AND_LOG(dev, 0, "server_writeable", "[SERVER_WRITEABLE] callback completed");
428 break;
429 }
430
431 case LWS_CALLBACK_RECEIVE: {
432 // Received data from client - may be fragmented for large messages
433 log_dev_every(4500 * US_PER_MS_INT, "LWS_CALLBACK_RECEIVE: conn_data=%p, transport=%p, len=%zu", (void *)conn_data,
434 conn_data ? (void *)conn_data->transport : NULL, len);
435
436 if (!conn_data) {
437 log_error("LWS_CALLBACK_RECEIVE: conn_data is NULL! Need to initialize from ESTABLISHED or handle here");
438 break;
439 }
440
441 // Check if cleanup is in progress to avoid race condition with remove_client
442 if (conn_data->cleaning_up) {
443 log_debug("LWS_CALLBACK_RECEIVE: Cleanup in progress, discarding received data");
444 break;
445 }
446
447 // Snapshot the transport pointer to avoid race condition with cleanup thread
448 acip_transport_t *transport_snapshot = conn_data->transport;
449 log_info("🔵 [WS_RECEIVE] conn_data=%p transport_snapshot=%p handler_started=%d", (void *)conn_data,
450 (void *)transport_snapshot, conn_data ? conn_data->handler_started : -1);
451 if (!transport_snapshot) {
452 SET_ERRNO(ERROR_INVALID_STATE, "Transport snapshot is NULL");
453 return -1;
454 }
455
456 if (!in || len == 0) {
457 break;
458 }
459
460 // Check if cleanup is already in progress to avoid race condition with LWS_CALLBACK_CLOSED
461 if (conn_data && conn_data->cleaning_up) {
462 log_debug("RECEIVE: Cleanup in progress, ignoring fragment");
463 break;
464 }
465
466 // Handler thread was spawned in LWS_CALLBACK_ESTABLISHED (or RECEIVE fallback)
467 // (It fires for server-side connections and properly initializes client_info_t)
468
469 // Get transport data using snapshotted pointer
470 websocket_transport_data_t *ws_data = (websocket_transport_data_t *)transport_snapshot->impl_data;
471 if (!ws_data || !ws_data->recv_queue) {
472 log_error("WebSocket transport has no implementation data or recv_queue (ws_data=%p, recv_queue=%p)",
473 (void *)ws_data, ws_data ? (void *)ws_data->recv_queue : NULL);
474 break;
475 }
476
477 uint64_t callback_enter_ns = time_get_ns(); // Capture entry time for duration measurement
478 log_debug("[WS_TIMING] callback_enter_ns captured: %llu", (unsigned long long)callback_enter_ns);
479
480 bool is_first = lws_is_first_fragment(wsi);
481 bool is_final = lws_is_final_fragment(wsi);
482 log_debug("[WS_TIMING] is_first=%d is_final=%d, about to increment callback count", is_first, is_final);
483 log_info("[WS_FRAG_DEBUG] === RECEIVE CALLBACK: is_first=%d is_final=%d len=%zu ===", is_first, is_final, len);
484
485 atomic_fetch_add_u64(&g_receive_callback_count, 1);
486 log_debug("[WS_TIMING] incremented callback count");
487
488 // OPTIMIZATION: Only enable TCP_QUICKACK on first fragment of each message.
489 // Re-enabling on EVERY fragment is too expensive (syscall overhead).
490 // Linux resets TCP_QUICKACK after each ACK, but we only need it for the first chunk.
491#ifdef __linux__
492 if (is_first) {
493 int fd = lws_get_socket_fd(wsi);
494 if (fd >= 0) {
495 int quickack = 1;
496 setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &quickack, sizeof(quickack));
497 }
498 }
499#endif
500
501 if (is_first) {
502 // Reset fragment counters at start of new message
503 atomic_store_u64(&g_writeable_callback_count, 0);
504 atomic_store_u64(&g_receive_callback_count, 1);
505 }
506
507 // Log fragment arrival
508 // Note: Timing calculation with global g_receive_first_fragment_ns is broken for multi-fragment
509 // messages and will be fixed with per-connection tracking
510 {
511 uint64_t frag_num = atomic_load_u64(&g_receive_callback_count);
512 log_info("[WS_FRAG] Fragment #%llu: %zu bytes (first=%d final=%d)", (unsigned long long)frag_num, len, is_first,
513 is_final);
514 }
515
517 msg.data = buffer_pool_alloc(NULL, len);
518 if (!msg.data) {
519 log_error("Failed to allocate buffer for fragment (%zu bytes)", len);
520 break;
521 }
522
523 memcpy(msg.data, in, len);
524 msg.len = len;
525 msg.first = is_first;
526 msg.final = is_final;
527
528 mutex_lock(&ws_data->recv_mutex);
529
530 if (!ws_data->recv_queue) {
531 log_warn("recv_queue was cleared during lock wait, dropping fragment");
532 mutex_unlock(&ws_data->recv_mutex);
533 buffer_pool_free(NULL, msg.data, msg.len);
534 break;
535 }
536
537 if (is_first) {
538 size_t queue_current_size = ringbuffer_size(ws_data->recv_queue);
539 size_t queue_capacity = ws_data->recv_queue->capacity;
540 size_t queue_free = queue_capacity - queue_current_size;
541 log_dev_every(4500 * US_PER_MS_INT, "[WS_FLOW] Queue: free=%zu/%zu (used=%zu)", queue_free, queue_capacity,
542 queue_current_size);
543 }
544
545 bool success = ringbuffer_write(ws_data->recv_queue, &msg);
546 if (!success) {
547 // Queue is full - drop fragment (flow control would deadlock the dispatch thread)
548 log_warn("[WS_FLOW] Receive queue FULL - dropping fragment (len=%zu, first=%d, final=%d)", len, is_first,
549 is_final);
550 buffer_pool_free(NULL, msg.data, msg.len);
551
552 mutex_unlock(&ws_data->recv_mutex);
553 break;
554 }
555
556 // Signal waiting recv() call that a fragment is available
557 log_dev("[WS_DEBUG] RECEIVE: About to signal recv_cond (queue size=%zu)", ringbuffer_size(ws_data->recv_queue));
558 cond_signal(&ws_data->recv_cond);
559 mutex_unlock(&ws_data->recv_mutex);
560 log_dev("[WS_DEBUG] RECEIVE: Unlocked recv_mutex");
561
562 // Signal handler to process queued fragments via the condition variable
563 // (cond_signal already called above - do not call lws_callback_on_writable here as it
564 // interferes with fragmented frame processing in libwebsockets)
565
566 log_info("[WS_FRAG] Queued fragment: %zu bytes (first=%d final=%d, total_fragments=%llu)", len, is_first, is_final,
567 (unsigned long long)atomic_load_u64(&g_receive_callback_count));
568
569 // Record timing for this callback
571 websocket_callback_timing_start());
572 break;
573 }
574
575 case LWS_CALLBACK_HTTP: {
576 // Serve /health endpoint for container health checks
577 const char *path = (const char *)in;
578 if (path && (strcmp(path, "/health") == 0 || strcmp(path, "health") == 0)) {
579 yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
580 yyjson_mut_val *root = yyjson_mut_obj(doc);
581 yyjson_mut_doc_set_root(doc, root);
582 yyjson_mut_obj_add_str(doc, root, "status", "ok");
583 yyjson_mut_obj_add_str(doc, root, "version", ASCII_CHAT_VERSION_FULL);
584 char proto_ver[16];
585 snprintf(proto_ver, sizeof(proto_ver), "%d.%d.%d", PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR,
587 yyjson_mut_obj_add_str(doc, root, "protocol", proto_ver);
588
589 size_t json_len = 0;
590 char *json = yyjson_mut_write(doc, YYJSON_WRITE_PRETTY_TWO_SPACES, &json_len);
591 yyjson_mut_doc_free(doc);
592 if (!json)
593 return -1;
594
595 unsigned char buf[LWS_PRE + 1024];
596 unsigned char *start = buf + LWS_PRE;
597 unsigned char *p = start;
598 unsigned char *end = buf + sizeof(buf);
599
600 if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
601 lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)"application/json", 16,
602 &p, end) ||
603 lws_add_http_header_content_length(wsi, (lws_filepos_t)json_len, &p, end) ||
604 lws_finalize_http_header(wsi, &p, end)) {
605 free(json);
606 return -1;
607 }
608
609 memcpy(p, json, json_len);
610 p += json_len;
611 free(json);
612
613 if (lws_write(wsi, start, (size_t)(p - start), LWS_WRITE_HTTP_HEADERS) < 0)
614 return -1;
615 if (lws_http_transaction_completed(wsi))
616 return -1;
617 return 0;
618 }
619 // Serve /stats endpoint for server statistics
620 else if (path && (strcmp(path, "/stats") == 0 || strcmp(path, "stats") == 0)) {
621 const struct lws_protocols *protocol = lws_get_protocol(wsi);
622 websocket_server_t *ws_server = protocol ? (websocket_server_t *)protocol->user : NULL;
623
624 yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
625 yyjson_mut_val *root = yyjson_mut_obj(doc);
626 yyjson_mut_doc_set_root(doc, root);
627
628 if (ws_server && ws_server->user_data) {
629 acds_server_t *acds = (acds_server_t *)ws_server->user_data;
630 yyjson_mut_val *network_obj = yyjson_mut_obj(doc);
631
632 uint64_t bytes_sent = atomic_load_u64(&acds->stats.bytes_sent);
633 uint64_t bytes_received = atomic_load_u64(&acds->stats.bytes_received);
634 uint64_t packets_sent = atomic_load_u64(&acds->stats.packets_sent);
635 uint64_t packets_received = atomic_load_u64(&acds->stats.packets_received);
636 time_t start_time = acds->stats.start_time;
637
638 yyjson_mut_obj_add_uint(doc, network_obj, "bytes_sent", bytes_sent);
639 yyjson_mut_obj_add_uint(doc, network_obj, "bytes_received", bytes_received);
640 yyjson_mut_obj_add_uint(doc, network_obj, "packets_sent", packets_sent);
641 yyjson_mut_obj_add_uint(doc, network_obj, "packets_received", packets_received);
642
643 time_t now = time(NULL);
644 long uptime_seconds = now - start_time;
645 yyjson_mut_obj_add_int(doc, network_obj, "uptime_seconds", uptime_seconds);
646
647 yyjson_mut_obj_add_val(doc, root, "network", network_obj);
648
649 if (acds->db) {
650 int64_t session_count = 0;
651 int64_t unique_clients = 0;
652
653 if (database_stats_session_count(acds->db, &session_count) == ASCIICHAT_OK) {
654 yyjson_mut_obj_add_int(doc, root, "active_sessions", session_count);
655 }
656
657 if (database_stats_unique_clients(acds->db, &unique_clients) == ASCIICHAT_OK) {
658 yyjson_mut_obj_add_int(doc, root, "unique_clients", unique_clients);
659 }
660 }
661 }
662
663 size_t json_len = 0;
664 char *json = yyjson_mut_write(doc, YYJSON_WRITE_PRETTY_TWO_SPACES, &json_len);
665 yyjson_mut_doc_free(doc);
666 if (!json)
667 return -1;
668
669 unsigned char buf[LWS_PRE + 4096];
670 unsigned char *start = buf + LWS_PRE;
671 unsigned char *p = start;
672 unsigned char *end = buf + sizeof(buf);
673
674 if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
675 lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)"application/json", 16,
676 &p, end) ||
677 lws_add_http_header_content_length(wsi, (lws_filepos_t)json_len, &p, end) ||
678 lws_finalize_http_header(wsi, &p, end)) {
679 free(json);
680 return -1;
681 }
682
683 memcpy(p, json, json_len);
684 p += json_len;
685 free(json);
686
687 if (lws_write(wsi, start, (size_t)(p - start), LWS_WRITE_HTTP_HEADERS) < 0)
688 return -1;
689 if (lws_http_transaction_completed(wsi))
690 return -1;
691 return 0;
692 }
693 // Reject other HTTP requests
694 lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, "Not Found");
695 if (lws_http_transaction_completed(wsi))
696 return -1;
697 return 0;
698 }
699
700 case LWS_CALLBACK_FILTER_HTTP_CONNECTION: {
701 // WebSocket upgrade handshake - allow all connections
702 log_info("[FILTER_HTTP_CONNECTION] WebSocket upgrade request (allow protocol upgrade)");
703 return 0; // Allow the connection
704 }
705
706 case LWS_CALLBACK_PROTOCOL_INIT: {
707 // Protocol initialization with timing instrumentation
708 uint64_t callback_start_ns = websocket_callback_timing_start();
709 log_info("[PROTOCOL_INIT] Protocol initialized, proto=%s", proto_name);
710 uint64_t callback_end_ns = websocket_callback_timing_start();
711 websocket_callback_timing_record(&g_ws_callback_timing.protocol_init, callback_start_ns, callback_end_ns);
712 break;
713 }
714
715 case LWS_CALLBACK_PROTOCOL_DESTROY: {
716 // Protocol destruction with timing instrumentation
717 uint64_t callback_start_ns = websocket_callback_timing_start();
718 log_info("[PROTOCOL_DESTROY] Protocol destroyed, proto=%s", proto_name);
719 uint64_t callback_end_ns = websocket_callback_timing_start();
720 websocket_callback_timing_record(&g_ws_callback_timing.protocol_destroy, callback_start_ns, callback_end_ns);
721 break;
722 }
723
724 case LWS_CALLBACK_EVENT_WAIT_CANCELLED: {
725 // Fired on the service thread when lws_cancel_service() is called from another thread.
726 // This is how we safely convert cross-thread send requests into writable callbacks.
727 // lws_callback_on_writable() is only safe from the service thread context.
728 //
729 // CRITICAL FIX: lws_cancel_service() wakes the event loop, which fires this callback.
730 // The `wsi` parameter may be the listening socket or any random connection, so we
731 // CANNOT rely on lws_get_protocol(wsi) to get the correct protocol.
732 // Instead, we MUST trigger WRITEABLE on ALL protocols that handle WebSocket frames:
733 // - "http" protocol (browser connects here for initial HTTP upgrade to WebSocket)
734 // - "acip" protocol (for ACIP over WebSocket connections)
735 // Both protocols use the same callback and handle frame transmission.
737 1 * NS_PER_SEC_INT,
738 ">>> LWS_CALLBACK_EVENT_WAIT_CANCELLED triggered - requesting writable callbacks for all protocols");
739
740 struct lws_context *ctx = lws_get_context(wsi);
741 if (!ctx) {
742 log_error("EVENT_WAIT_CANCELLED: Could not get context from wsi");
743 break;
744 }
745
746 // Trigger WRITEABLE on both protocols
747 for (int i = 0; i < 2; i++) {
749 ">>> EVENT_WAIT_CANCELLED: Calling lws_callback_on_writable_all_protocol for protocol '%s'",
750 websocket_protocols[i].name);
751 lws_callback_on_writable_all_protocol(ctx, &websocket_protocols[i]);
752 }
753 break;
754 }
755
756 default:
757 break;
758 }
759
760 return 0;
761}
762
766static struct lws_protocols websocket_protocols[] = {
767 {
768 "http", // Default HTTP protocol (for WebSocket upgrade)
769 websocket_server_callback, // Use same callback for all protocols
770 sizeof(websocket_connection_data_t), // Per-session data size
771 524288, // RX buffer size (512KB for video frames)
772 0, // ID (auto-assigned)
773 NULL, // User pointer (set to server instance)
774 524288 // TX packet size (512KB for video frames)
775 },
776 {
777 "acip", // ACIP WebSocket subprotocol (after HTTP upgrade)
778 websocket_server_callback, // Callback function
779 sizeof(websocket_connection_data_t), // Per-session data size
780 524288, // RX buffer size (512KB for video frames)
781 0, // ID (auto-assigned)
782 NULL, // User pointer (set to server instance)
783 524288 // TX packet size (512KB for video frames)
784 },
785 {NULL, NULL, 0, 0, 0, NULL, 0} // Terminator
786};
787
812static const lws_retry_bo_t keep_alive_policy = {
813 .secs_since_valid_ping = 30, // Send PING after 30s idle
814 .secs_since_valid_hangup = 35, // Hangup if still idle after 35s
815};
816
818 if (!server || !config || !config->client_handler) {
819 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
820 }
821
822 memset(server, 0, sizeof(*server));
823 server->handler = config->client_handler;
824 server->user_data = config->user_data;
825 server->port = config->port;
826 atomic_store_bool(&server->running, true);
827
828 // Store server pointer in protocol user data so callbacks can access it
829 websocket_protocols[0].user = server; // http protocol
830 websocket_protocols[1].user = server; // acip protocol
831
832 // Enable libwebsockets logging through centralized logging system
834
835 // HTTP mount for /health endpoint (required for LWS_CALLBACK_HTTP to fire)
836 static const struct lws_http_mount health_mount = {
837 .mount_next = NULL,
838 .mountpoint = "/",
839 .mountpoint_len = 1,
840 .origin = "http",
841 .def = NULL,
842 .protocol = "http",
843 .origin_protocol = LWSMPRO_CALLBACK,
844 };
845
846 // Configure libwebsockets context
847 struct lws_context_creation_info info = {0};
848 info.port = config->port;
849 info.protocols = websocket_protocols;
850 info.mounts = &health_mount;
851 info.gid = (gid_t)-1; // Cast to avoid undefined behavior with unsigned type
852 info.uid = (uid_t)-1; // Cast to avoid undefined behavior with unsigned type
853 info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; // Initialize SSL/TLS support (required for server binding)
854 info.extensions = NULL; // Disable permessage-deflate - causing connection issues
855 info.retry_and_idle_policy = &keep_alive_policy; // Configure keep-alive to prevent idle disconnects during handshake
856
857 // Configure TLS/WSS support if certificates are provided (check for non-empty strings)
858 if (config->tls_cert_path && config->tls_cert_path[0] != '\0' && config->tls_key_path &&
859 config->tls_key_path[0] != '\0') {
860#ifdef LWS_WITH_TLS
861 // Validate certificate and key files are readable
862 if (platform_access(config->tls_cert_path, R_OK) != 0) {
863 return SET_ERRNO(ERROR_NETWORK_BIND, "TLS certificate file not readable: %s (errno=%d)", config->tls_cert_path,
864 errno);
865 }
866 if (platform_access(config->tls_key_path, R_OK) != 0) {
867 return SET_ERRNO(ERROR_NETWORK_BIND, "TLS key file not readable: %s (errno=%d)", config->tls_key_path, errno);
868 }
869
870 info.ssl_cert_filepath = config->tls_cert_path;
871 info.ssl_private_key_filepath = config->tls_key_path;
872 log_info("WebSocket server configured for WSS (TLS): cert=%s, key=%s", config->tls_cert_path, config->tls_key_path);
873#else
874 log_warn("WebSocket server: TLS support not compiled in libwebsockets; WSS unavailable");
875#endif
876 } else if ((config->tls_cert_path && config->tls_cert_path[0] != '\0') ||
877 (config->tls_key_path && config->tls_key_path[0] != '\0')) {
878 log_warn("WebSocket server: Both TLS certificate and key must be provided for WSS; using plain WS");
879 }
880
881 // Increase per-thread service buffer to prevent fragmentation of large messages
882 // Default is 4KB, causing large frames to fragment into many chunks
883 // Video frames are 921KB, so increase to 2MB to handle them without fragmentation
884 info.pt_serv_buf_size = 2 * 1024 * 1024; // 2MB per-thread service buffer
885
886 // Disable ALL default timeouts and keep-alive mechanisms
887 // Use only the explicit retry_and_idle_policy we set above (30/35 seconds)
888 info.ka_time = 0; // Disable TCP keep-alive probes (use WebSocket pings instead)
889 info.ka_probes = 0; // Disable TCP probes
890 info.ka_interval = 0; // Disable TCP probe intervals
891 info.keepalive_timeout = 0; // Disable HTTP keep-alive timeout entirely
892
893 // Explicitly set a very large idle timeout to prevent early disconnection
894 // Without this, libwebsockets defaults to 5 seconds for HTTP connections
895 // Set to effectively infinite (100 hours) since we use retry_and_idle_policy instead
896 info.timeout_secs = 360000; // 100 hours - effectively disabled
897
898 // Create libwebsockets context
899 // Log diagnostic info before attempting creation
900 log_debug("lws_create_context: port=%u, protocols=%p, options=0x%x", info.port, (void *)info.protocols, info.options);
901
902 server->context = lws_create_context(&info);
903 if (!server->context) {
904 // Capture any OpenSSL errors that may have occurred during context creation
905 unsigned long ssl_err = 0;
906 char ssl_err_str[256] = "no OpenSSL errors in queue";
907
908 // Extract all errors from OpenSSL error queue
909 while ((ssl_err = ERR_get_error()) != 0) {
910 ERR_error_string_n(ssl_err, ssl_err_str, sizeof(ssl_err_str));
911 log_error("OpenSSL error: %lu: %s", ssl_err, ssl_err_str);
912 }
913
914 return SET_ERRNO(ERROR_NETWORK_BIND, "Failed to create libwebsockets context (last OpenSSL error: %s)",
915 ssl_err_str);
916 }
917
918 // Create thread pool for handling client connections
919 // Use 4 worker threads to handle concurrent client handlers without blocking LWS event loop
920 server->handler_pool = thread_pool_create_with_workers("websocket_handlers", 4);
921 if (!server->handler_pool) {
922 lws_context_destroy(server->context);
923 server->context = NULL;
924 return SET_ERRNO(ERROR_THREAD, "Failed to create WebSocket handler thread pool");
925 }
926
927 /* Register WebSocket server with named registry */
928 char ws_port_name[32];
929 snprintf(ws_port_name, sizeof(ws_port_name), "ws:%d", server->port);
930 NAMED_REGISTER(server, ws_port_name, "websocket_server", "0x%tx", NULL);
931
932 /* Register server's sync primitives with hierarchical naming */
933 NAMED_REGISTER_ATOMIC(&server->running, "is_running", (uintptr_t)(const void *)(server));
934
935 /* Register WebSocket context as child of server */
936 NAMED_REGISTER_WEBSOCKET(server->context, "context", (uintptr_t)(const void *)(server));
937
938 log_info("WebSocket server initialized on port %d with static file serving", config->port);
939 return ASCIICHAT_OK;
940}
941
943 if (!server || !server->context) {
944 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid server");
945 }
946
947 log_info("WebSocket server starting event loop on port %d", server->port);
948
949 // Run libwebsockets event loop
950 uint64_t last_service_ns = 0;
951 int service_call_count = 0;
952 while (atomic_load_bool(&server->running)) {
953 // Service libwebsockets with 16ms timeout to match 60 FPS frame rate.
954 // This provides frequent event processing (~60 callback invocations per second) so that
955 // WebSocket frames queued by the render loop are sent promptly, matching TCP performance.
956 // All client connections share this single server context, so a single lws_service() call
957 // services fragments and events for all clients simultaneously.
958 uint64_t service_start_ns = time_get_ns();
959 if (last_service_ns && service_start_ns - last_service_ns > 30 * US_PER_MS_INT) {
960 // > 30ms gap between service calls
961 double gap_ms = (double)(service_start_ns - last_service_ns) / 1e6;
962 log_info_every(1 * US_PER_MS_INT, "[LWS_SERVICE_GAP] %.1fms gap between lws_service calls", gap_ms);
963 }
964 service_call_count++;
965 log_debug_every(500 * US_PER_MS_INT, "[LWS_SERVICE] Call #%d, context=%s", service_call_count,
966 NAMED_DESCRIBE(server->context, "websocket_server"));
967 int result = lws_service(server->context, 1);
968 if (result < 0) {
969 log_error("libwebsockets service error: %d", result);
970 break;
971 }
972 }
973
974 log_info(
975 "WebSocket server event loop exited (context will be destroyed by main thread after handler threads complete)");
976 return ASCIICHAT_OK;
977}
978
980 if (server && server->context) {
981 lws_cancel_service(server->context);
982 }
983}
984
986 if (!server) {
987 return;
988 }
989
990 atomic_store_bool(&server->running, false);
991
992 // Cancel any pending libwebsockets service calls to interrupt blocking lws_service()
993 // This prevents the event loop from getting stuck in lws_service()
994 if (server->context) {
995 log_debug("[WEBSOCKET_SERVER_DESTROY] Cancelling libwebsockets service");
996 lws_cancel_service(server->context);
997 }
998
999 // Unregister atomic fields
1000 NAMED_UNREGISTER(&server->running);
1001
1002 // Destroy handler thread pool (waits for pending work to complete)
1003 if (server->handler_pool) {
1005 server->handler_pool = NULL;
1006 }
1007
1008 // Context is normally destroyed by websocket_server_run (from the event loop
1009 // thread) for fast shutdown. This handles the case where run() wasn't called
1010 // or didn't complete normally.
1011 if (server->context) {
1012 NAMED_UNREGISTER(server->context);
1013 log_debug("WebSocket context still alive in destroy, cleaning up");
1014 lws_context_destroy(server->context);
1015 server->context = NULL;
1016 }
1017
1018 log_debug("WebSocket server destroyed");
1019}
Platform abstraction layer umbrella header providing unified cross-platform API.
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
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
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
websocket_callback_timing_t g_ws_callback_timing
Global timing tracker - shared across all WebSocket connections.
void websocket_callback_timing_record(websocket_callback_stats_t *stats, uint64_t start_ns, uint64_t end_ns)
Record callback invocation.
WebSocket callback timing instrumentation.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Cross-platform condition variable interface for ascii-chat.
Named object registry for debugging — log identifiable resource names.
asciichat_error_t database_stats_unique_clients(sqlite3 *db, int64_t *count_out)
Get total number of unique clients/participants.
asciichat_error_t database_stats_session_count(sqlite3 *db, int64_t *count_out)
Get total number of active sessions.
Cross-platform error handling utilities.
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
unsigned short uint16_t
Definition common.h:57
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_STRERROR(errnum)
Definition common.h:465
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t 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 NAMED_DESCRIBE(ptr, hint)
Describe a pointer in log format.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define NAMED_REGISTER_WEBSOCKET(websocket, name, parent_ptr)
Register a WebSocket connection with automatic format specifier.
#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
@ 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_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#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 STOP_TIMER_AND_LOG(log_level, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with a custom message.
Definition time.h:612
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define US_PER_MS_INT
Definition time.h:160
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_access(const char *pathname, int mode)
Check file/directory access permissions.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
int errno
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define PROTOCOL_VERSION_MAJOR
Major protocol version number.
#define PROTOCOL_VERSION_MINOR
Minor protocol version number.
#define PROTOCOL_VERSION_PATCH
Patch protocol version number.
size_t ringbuffer_size(const ringbuffer_t *rb)
Get current number of elements in the buffer.
Definition ringbuffer.c:121
bool ringbuffer_is_empty(const ringbuffer_t *rb)
Check if buffer is empty.
Definition ringbuffer.c:125
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Try to read an element from the ring buffer (non-blocking)
Definition ringbuffer.c:82
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking)
Definition ringbuffer.c:60
WebSocket server for accepting browser client connections.
asciichat_error_t websocket_server_init(websocket_server_t *server, const websocket_server_config_t *config)
Initialize WebSocket server.
asciichat_error_t websocket_server_run(websocket_server_t *server)
Run WebSocket server event loop.
void websocket_server_destroy(websocket_server_t *server)
Destroy WebSocket server and free resources.
void websocket_server_cancel_service(websocket_server_t *server)
Wake the WebSocket server event loop immediately.
void * asciichat_thread_t
📝 Logging API with multiple log levels and terminal output control
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
Definition log/log.h:705
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
Internal WebSocket implementation types shared between transport.c and server.c.
Cross-platform mutex interface for ascii-chat.
Protocol version, features, compression, and frame flag constants.
Lock-Free Ring Buffer and Frame Buffer Management.
Discovery server state.
Transport instance structure.
Definition transport.h:214
void * impl_data
Transport-specific state.
Definition transport.h:218
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
websocket_callback_stats_t receive
websocket_callback_stats_t protocol_destroy
websocket_callback_stats_t server_writeable
websocket_callback_stats_t protocol_init
Context passed to client handler thread.
acip_transport_t * transport
ACIP transport for this client.
char client_ip[64]
Client IP address (for logging/rate limiting)
bool auth_required
True if client authentication is required.
void * user_data
User-provided data from server config.
bool is_secure
True if connection is using TLS (wss://)
asciichat_thread_t handler_thread
Client handler thread.
bool cleaning_up
True if cleanup is in progress (prevents race with remove_client)
bool handler_started
True if handler thread was started.
acip_transport_t * transport
ACIP transport for this connection.
uint8_t * pending_send_data
Current message being sent.
bool has_pending_send
True if there's an in-progress message.
size_t pending_send_len
Total length of message being sent.
websocket_server_t * server
Back-reference to server.
size_t pending_send_offset
Current offset in message (bytes already sent)
WebSocket message element (individual frame or fragment)
size_t len
Frame length in bytes.
uint8_t first
1 if first frame of message (or complete message)
uint8_t final
1 if final frame of message (or complete message)
uint8_t * data
Frame data (allocated, caller must free)
websocket_client_handler_fn client_handler
Handler for new connections.
void * user_data
User data passed to handlers.
const char * tls_cert_path
TLS certificate file path (optional, for wss://)
const char * tls_key_path
TLS private key file path (optional, for wss://)
websocket_client_handler_fn handler
Client handler function.
struct thread_pool * handler_pool
Thread pool for queueing handler work.
struct lws_context * context
libwebsockets context
WebSocket transport implementation data.
void thread_pool_destroy(thread_pool_t *pool)
Destroy a thread pool.
void acip_transport_destroy(acip_transport_t *transport)
Destroy transport and free all resources.
asciichat_error_t thread_pool_queue_work(const char *name, thread_pool_t *pool, void *(*work_func)(void *), void *work_arg)
Queue a named work item to a work queue pool.
thread_pool_t * thread_pool_create_with_workers(const char *pool_name, size_t num_workers)
Create a new thread pool in work queue mode.
⏱️ High-precision timing utilities using sokol_time.h and uthash
Transport abstraction layer for ACIP protocol.
#define ASCII_CHAT_VERSION_FULL
Definition version.h:21
acip_transport_t * acip_websocket_server_transport_create(const char *name, struct lws *wsi, crypto_context_t *crypto_ctx)
Create WebSocket server transport from existing connection.
void lws_log_init_server(void)
Initialize libwebsockets logging for server mode.
Definition websocket.c:87
Libwebsockets logging integration.