master
c 638 lines 24.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "daemon/daemon-service.h"
4 #include "websocket-internal.h"
5
6 static void websocket_thread_client_socket_error(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc, const char *reason) {
7 internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
8
9 worker_is_busy(WORKERS_WEBSOCKET_SOCK_ERROR);
10
11 websocket_debug(wsc, reason);
12
13 // Send command to remove the client
14 // Note: on_disconnect will be called in websocket_thread_remove_client
15 websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
16 }
17
18 // Add a client to a thread's poll
19 static bool websocket_thread_add_client(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc) {
20 internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
21
22 // Initialize compression with the parsed options
23 websocket_compression_init(wsc);
24 websocket_decompression_init(wsc);
25
26 // Add client to the poll - use the socket fd directly
27 bool added = nd_poll_add(wth->ndpl, wsc->sock.fd, ND_POLL_READ, wsc);
28 if(!added) {
29 websocket_error(wsc, "Failed to add client to poll");
30 return false;
31 }
32
33 // Add client to the thread's client list
34 DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(wth->clients, wsc, prev, next);
35
36 return true;
37 }
38
39 static void websocket_thread_remove_client(WEBSOCKET_THREAD *wth, WS_CLIENT *wsc, bool have_lock) {
40 internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
41
42 // Notify the protocol handler that the client is being disconnected
43 if (wsc->on_disconnect) {
44 websocket_debug(wsc, "Calling on_disconnect callback for protocol %s", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
45 wsc->on_disconnect(wsc);
46 }
47
48 // send a close frame (it won't do it if not allowed by the protocol)
49 websocket_protocol_send_close(wsc, WS_CLOSE_NORMAL, "Connection closed by server");
50
51 // If already in a closing state, just flush any pending data
52 websocket_write_data(wsc);
53
54 // Remove client from the poll - use socket fd directly
55 bool removed = nd_poll_del(wth->ndpl, wsc->sock.fd);
56 if(!removed) {
57 websocket_debug(wsc, "Failed to remove client %zu from poll", wsc->id);
58 }
59
60 websocket_decompression_cleanup(wsc);
61 websocket_compression_cleanup(wsc);
62
63 // Remove client from the thread's client list
64 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(wth->clients, wsc, prev, next);
65
66 // Lock the thread clients
67 if(!have_lock)
68 spinlock_lock(&wth->clients_spinlock);
69
70 if(wth->clients_current > 0)
71 wth->clients_current--;
72
73 // Release the thread clients lock
74 if(!have_lock)
75 spinlock_unlock(&wth->clients_spinlock);
76
77 websocket_debug(wsc, "Removed and resources freed", wth->id, wsc->id);
78 websocket_client_free(wsc);
79 }
80
81 // Update a client's poll event flags
82 bool websocket_thread_update_client_poll_flags(WS_CLIENT *wsc) {
83 if(!wsc || !wsc->wth || wsc->sock.fd < 0)
84 return false;
85
86 internal_fatal(wsc->wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
87
88 nd_poll_event_t events = wsc->flush_and_remove_client ? 0 : ND_POLL_READ;
89 if(cbuffer_next_unsafe(&wsc->out_buffer, NULL) > 0)
90 events |= ND_POLL_WRITE;
91
92 // Update poll events
93 bool updated = nd_poll_upd(wsc->wth->ndpl, wsc->sock.fd, events);
94 if(!updated)
95 websocket_error(wsc, "Failed to update poll events for client");
96
97 return updated;
98 }
99
100 struct pipe_header {
101 uint8_t cmd;
102 union {
103 uint32_t id;
104 uint32_t len;
105 };
106 };
107
108 static ssize_t write_pipe_block(int fd, const void *buffer, size_t size) {
109 const char *buf = buffer;
110 ssize_t total_written = 0;
111
112 while (total_written < (ssize_t) size) {
113 ssize_t bytes = write(fd, buf + total_written, size - total_written);
114
115 if (bytes < 0) {
116 if (errno == EINTR)
117 continue;
118 if (errno == EAGAIN || errno == EWOULDBLOCK)
119 return total_written;
120 return -1;
121 }
122 else if (bytes == 0)
123 return total_written;
124
125 total_written += bytes;
126 }
127
128 return total_written;
129 }
130
131 // Send command to a thread
132 bool websocket_thread_send_command(WEBSOCKET_THREAD *wth, uint8_t cmd, uint32_t id) {
133 if(!wth || wth->cmd.pipe[PIPE_WRITE] == -1) {
134 netdata_log_error("WEBSOCKET[%zu]: Failed to send command - pipe is not initialized", wth ? wth->id : 0);
135 return false;
136 }
137
138 // Prepare command
139 struct pipe_header header = {
140 .cmd = cmd,
141 .id = id,
142 };
143
144 // Lock command pipe for writing
145 spinlock_lock(&wth->spinlock);
146
147 // Write command header
148 ssize_t bytes = write_pipe_block(wth->cmd.pipe[PIPE_WRITE], &header, sizeof(header));
149 if(bytes != (ssize_t)sizeof(header)) {
150 netdata_log_error("WEBSOCKET[%zu]: Failed to write command header to pipe", wth->id);
151 spinlock_unlock(&wth->spinlock);
152 return false;
153 }
154
155 // Release command pipe
156 spinlock_unlock(&wth->spinlock);
157
158 return true;
159 }
160
161 bool websocket_thread_send_broadcast(WEBSOCKET_THREAD *wth, WEBSOCKET_OPCODE opcode, const char *message) {
162 if(!wth || !message || wth->cmd.pipe[PIPE_WRITE] == -1) {
163 netdata_log_error("WEBSOCKET[%zu]: Failed to send command - pipe is not initialized", wth ? wth->id : 0);
164 return false;
165 }
166
167 size_t message_len_sz = strlen(message);
168 if(message_len_sz > UINT32_MAX || message_len_sz > WS_MAX_OUTGOING_FRAME_SIZE) {
169 netdata_log_error("WEBSOCKET[%zu]: Broadcast message too large: %zu bytes", wth ? wth->id : 0, message_len_sz);
170 return false;
171 }
172 uint32_t message_len = (uint32_t)message_len_sz;
173
174 // Prepare command
175 struct pipe_header header = {
176 .cmd = WEBSOCKET_THREAD_CMD_BROADCAST,
177 .len = sizeof(opcode) + message_len,
178 };
179
180 // Lock command pipe for writing
181 spinlock_lock(&wth->spinlock);
182
183 // Write command header
184 ssize_t bytes = write_pipe_block(wth->cmd.pipe[PIPE_WRITE], &header, sizeof(header));
185 if(bytes != (ssize_t)sizeof(header)) {
186 netdata_log_error("WEBSOCKET[%zu]: Failed to write command header to pipe", wth->id);
187 spinlock_unlock(&wth->spinlock);
188 return false;
189 }
190
191 // Write the opcode
192 bytes = write_pipe_block(wth->cmd.pipe[PIPE_WRITE], &opcode, sizeof(opcode));
193 if(bytes != (ssize_t)sizeof(opcode)) {
194 netdata_log_error("WEBSOCKET[%zu]: Failed to write broadcast opcode to pipe", wth->id);
195 spinlock_unlock(&wth->spinlock);
196 return false;
197 }
198
199 // Write the message
200 bytes = write_pipe_block(wth->cmd.pipe[PIPE_WRITE], message, message_len);
201 if(bytes != (ssize_t)message_len) {
202 netdata_log_error("WEBSOCKET[%zu]: Failed to write broadcast message to pipe", wth->id);
203 spinlock_unlock(&wth->spinlock);
204 return false;
205 }
206
207 // Release command pipe
208 spinlock_unlock(&wth->spinlock);
209
210 return true;
211 }
212
213 static ssize_t read_pipe_block(int fd, void *buffer, size_t size) {
214 char *buf = buffer;
215 ssize_t total_read = 0;
216
217 while (total_read < (ssize_t) size) {
218 ssize_t bytes = read(fd, buf + total_read, size - total_read);
219
220 if (bytes < 0) {
221 if (errno == EAGAIN || errno == EWOULDBLOCK) {
222 // Non-blocking case, return what we've read so far
223 return total_read;
224 }
225
226 // Real error occurred
227 return -1;
228
229 }
230 else if (bytes == 0)
231 return total_read;
232
233 total_read += bytes;
234 }
235
236 return total_read;
237 }
238
239 // Process a thread's command pipe
240 static void websocket_thread_process_commands(WEBSOCKET_THREAD *wth) {
241 internal_fatal(wth->tid != gettid_cached(), "Function %s() should only be used by the websocket thread", __FUNCTION__ );
242
243 struct pipe_header header;
244
245 // Read all available commands
246 for(;;) {
247 // Read command header
248
249 worker_is_busy(WORKERS_WEBSOCKET_CMD_READ);
250
251 ssize_t bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], &header, sizeof(header));
252 if(bytes <= 0) {
253 if(bytes < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
254 netdata_log_error("WEBSOCKET[%zu]: Failed to read command header from pipe", wth->id);
255 }
256 break;
257 }
258
259 if(bytes != sizeof(header)) {
260 netdata_log_error("WEBSOCKET[%zu]: Read partial command header (%zd/%zu bytes)", wth->id, bytes, sizeof(header));
261 break;
262 }
263
264 // Process command
265 switch(header.cmd) {
266 case WEBSOCKET_THREAD_CMD_EXIT:
267 worker_is_busy(WORKERS_WEBSOCKET_CMD_EXIT);
268 netdata_log_info("WEBSOCKET[%zu] received exit command", wth->id);
269 return;
270
271 case WEBSOCKET_THREAD_CMD_ADD_CLIENT: {
272 worker_is_busy(WORKERS_WEBSOCKET_CMD_ADD);
273 WS_CLIENT *wsc = websocket_client_find_by_id(header.id);
274 if(!wsc) {
275 netdata_log_error("WEBSOCKET[%zu]: Client %u not found for add command", wth->id, header.id);
276 continue;
277 }
278 internal_fatal(wsc->wth != wth, "Client %u added to wrong thread", header.id);
279 wsc->wth = wth;
280 if (websocket_thread_add_client(wth, wsc)) {
281 // Call the on_connect callback if provided to notify protocol handler of new client
282 if (wsc->on_connect) {
283 websocket_debug(wsc, "Calling on_connect callback for protocol %s", WEBSOCKET_PROTOCOL_2str(wsc->protocol));
284 wsc->on_connect(wsc);
285 }
286 }
287 break;
288 }
289
290 case WEBSOCKET_THREAD_CMD_REMOVE_CLIENT: {
291 worker_is_busy(WORKERS_WEBSOCKET_CMD_DEL);
292 WS_CLIENT *wsc = websocket_client_find_by_id(header.id);
293 if(!wsc) {
294 netdata_log_error("WEBSOCKET[%zu]: Client %u not found for remove command", wth->id, header.id);
295 continue;
296 }
297
298 websocket_thread_remove_client(wth, wsc, false);
299 break;
300 }
301
302 case WEBSOCKET_THREAD_CMD_BROADCAST: {
303 worker_is_busy(WORKERS_WEBSOCKET_CMD_BROADCAST);
304
305 WEBSOCKET_OPCODE opcode;
306 bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], &opcode, sizeof(opcode));
307 if(bytes != sizeof(opcode)) {
308 netdata_log_error("WEBSOCKET[%zu]: Failed to read broadcast opcode from pipe", wth->id);
309 continue;
310 }
311
312 if(header.len < sizeof(opcode)) {
313 netdata_log_error("WEBSOCKET[%zu]: Broadcast command header.len %u is too small", wth->id, header.len);
314 continue;
315 }
316 uint32_t message_len = header.len - sizeof(opcode);
317 if(message_len > WS_MAX_OUTGOING_FRAME_SIZE) {
318 netdata_log_error("WEBSOCKET[%zu]: Broadcast message too large: %u bytes", wth->id, message_len);
319 // Drain the payload to keep the pipe synchronized for subsequent commands.
320 char drain_buf[4096];
321 uint32_t remaining = message_len;
322 while(remaining > 0) {
323 size_t chunk = (remaining < sizeof(drain_buf)) ? remaining : sizeof(drain_buf);
324 ssize_t drained = read_pipe_block(wth->cmd.pipe[PIPE_READ], drain_buf, chunk);
325 if(drained <= 0) {
326 // Cannot complete drain: close both pipe ends so the next poll cycle does not
327 // try to parse the leftover payload bytes as a new pipe_header, and so that
328 // future write attempts fail the FD guard rather than hitting EPIPE.
329 netdata_log_error("WEBSOCKET[%zu]: Failed to fully drain oversized broadcast payload, closing command pipe to avoid desynchronization", wth->id);
330 if(wth->cmd.pipe[PIPE_READ] != -1) {
331 close(wth->cmd.pipe[PIPE_READ]);
332 wth->cmd.pipe[PIPE_READ] = -1;
333 }
334 if(wth->cmd.pipe[PIPE_WRITE] != -1) {
335 close(wth->cmd.pipe[PIPE_WRITE]);
336 wth->cmd.pipe[PIPE_WRITE] = -1;
337 }
338 return;
339 }
340 remaining -= (uint32_t)drained;
341 }
342 continue;
343 }
344 if(message_len + 1 > wth->cmd.buffer_size) {
345 wth->cmd.buffer = reallocz(wth->cmd.buffer, message_len + 1);
346 wth->cmd.buffer_size = message_len + 1;
347 }
348
349 char *message = wth->cmd.buffer;
350 bytes = read_pipe_block(wth->cmd.pipe[PIPE_READ], message, message_len);
351 if(bytes != message_len) {
352 netdata_log_error("WEBSOCKET[%zu]: Failed to read broadcast message from pipe", wth->id);
353 continue;
354 }
355 message[message_len] = '\0';
356
357 // Send to all clients in this thread
358 spinlock_lock(&wth->clients_spinlock);
359
360 WS_CLIENT *wsc = wth->clients;
361 while(wsc) {
362 if(wsc->state == WS_STATE_OPEN) {
363 websocket_send_message(wsc, message, message_len, opcode);
364 }
365 wsc = wsc->next;
366 }
367
368 spinlock_unlock(&wth->clients_spinlock);
369 break;
370 }
371
372 default:
373 worker_is_busy(WORKERS_WEBSOCKET_CMD_UNKNOWN);
374 netdata_log_error("WEBSOCKET[%zu]: Unknown command %u", wth->id, header.cmd);
375 break;
376 }
377 }
378 }
379
380 // Thread main function
381 void websocket_thread(void *ptr) {
382 WEBSOCKET_THREAD *wth = (WEBSOCKET_THREAD *)ptr;
383 wth->tid = gettid_uncached();
384
385 worker_register("WEBSOCKET");
386 worker_register_job_name(WORKERS_WEBSOCKET_POLL, "poll");
387 worker_register_job_name(WORKERS_WEBSOCKET_CMD_READ, "cmd read");
388 worker_register_job_name(WORKERS_WEBSOCKET_CMD_EXIT, "cmd exit");
389 worker_register_job_name(WORKERS_WEBSOCKET_CMD_ADD, "cmd add");
390 worker_register_job_name(WORKERS_WEBSOCKET_CMD_DEL, "cmd del");
391 worker_register_job_name(WORKERS_WEBSOCKET_CMD_BROADCAST, "cmd bcast");
392 worker_register_job_name(WORKERS_WEBSOCKET_CMD_UNKNOWN, "cmd unknown");
393 worker_register_job_name(WORKERS_WEBSOCKET_SOCK_RECEIVE, "ws rcv");
394 worker_register_job_name(WORKERS_WEBSOCKET_SOCK_SEND, "ws snd");
395 worker_register_job_name(WORKERS_WEBSOCKET_SOCK_ERROR, "ws err");
396 worker_register_job_name(WORKERS_WEBSOCKET_CLIENT_TIMEOUT, "client timeout");
397 worker_register_job_name(WORKERS_WEBSOCKET_SEND_PING, "send ping");
398 worker_register_job_name(WORKERS_WEBSOCKET_CLIENT_STUCK, "client stuck");
399 worker_register_job_name(WORKERS_WEBSOCKET_INCOMPLETE_FRAME, "incomplete frame");
400 worker_register_job_name(WORKERS_WEBSOCKET_COMPLETE_FRAME, "complete frame");
401 worker_register_job_name(WORKERS_WEBSOCKET_MESSAGE, "message");
402 worker_register_job_name(WORKERS_WEBSOCKET_MSG_PING, "rx ping");
403 worker_register_job_name(WORKERS_WEBSOCKET_MSG_PONG, "rx pong");
404 worker_register_job_name(WORKERS_WEBSOCKET_MSG_CLOSE, "rx close");
405 worker_register_job_name(WORKERS_WEBSOCKET_MSG_INVALID, "rx invalid");
406
407 time_t last_cleanup = now_monotonic_sec();
408
409 // Main thread loop
410 while(service_running(SERVICE_STREAMING) && !nd_thread_signaled_to_cancel()) {
411
412 worker_is_idle();
413
414 // Poll for events
415 nd_poll_result_t ev;
416 int rc = nd_poll_wait(wth->ndpl, 100, &ev); // 100ms timeout
417
418 worker_is_busy(WORKERS_WEBSOCKET_POLL);
419
420 if(rc < 0) {
421 if(errno == EAGAIN || errno == EINTR)
422 continue;
423
424 netdata_log_error("WEBSOCKET[%zu]: Poll error: %s", wth->id, strerror(errno));
425 break;
426 }
427
428 // Process poll events
429 if(rc > 0) {
430 // Handle command pipe
431 if(ev.data == &wth->cmd) {
432 if(ev.events & ND_POLL_READ) {
433 // Read and process commands
434 websocket_thread_process_commands(wth);
435 }
436 continue;
437 }
438
439 // Handle client events
440 WS_CLIENT *wsc = (WS_CLIENT *)ev.data;
441 if(!wsc) {
442 netdata_log_error("WEBSOCKET[%zu]: Poll event with NULL client data", wth->id);
443 continue;
444 }
445
446 // Push client connection info to log stack for all subsequent logs
447 ND_LOG_STACK lgs[] = {
448 ND_LOG_FIELD_U64(NDF_CONNECTION_ID, wsc->id),
449 ND_LOG_FIELD_TXT(NDF_SRC_IP, wsc->client_ip),
450 ND_LOG_FIELD_TXT(NDF_SRC_PORT, wsc->client_port),
451 ND_LOG_FIELD_END(),
452 };
453 ND_LOG_STACK_PUSH(lgs);
454
455 // Check for errors
456 if(ev.events & ND_POLL_HUP) {
457 websocket_thread_client_socket_error(wth, wsc, "Client hangup");
458 continue;
459 }
460 if(ev.events & ND_POLL_ERROR) {
461 websocket_thread_client_socket_error(wth, wsc, "Socket error");
462 continue;
463 }
464
465 // Process read events
466 if(ev.events & ND_POLL_READ) {
467 if(websocket_receive_data(wsc) < 0) {
468 websocket_thread_client_socket_error(wth, wsc, "Failed to receive data");
469 continue;
470 }
471 }
472
473 // Process write events
474 if(ev.events & ND_POLL_WRITE) {
475 if(websocket_write_data(wsc) < 0) {
476 websocket_thread_client_socket_error(wth, wsc, "Failed to send data");
477 continue;
478 }
479
480 // Check if this client is waiting to be closed after flushing outgoing data
481 if(wsc->flush_and_remove_client && cbuffer_used_size_unsafe(&wsc->out_buffer) == 0) {
482 // All data flushed - remove client
483 websocket_thread_remove_client(wth, wsc, false);
484 }
485 }
486 }
487
488 worker_is_idle();
489
490 // Periodic cleanup and health checks (every 30 seconds)
491 time_t now = now_monotonic_sec();
492 if(now - last_cleanup > 30) {
493 // Iterate through all clients in this thread
494 spinlock_lock(&wth->clients_spinlock);
495
496 WS_CLIENT *wsc = wth->clients;
497 while(wsc) {
498 WS_CLIENT *next = wsc->next; // Save next in case we remove this client
499
500 if(wsc->state == WS_STATE_OPEN) {
501 // Check if client is idle (no activity for over WS_IDLE_CHECK_INTERVAL seconds)
502 if(now - wsc->last_activity_t > WS_IDLE_CHECK_INTERVAL) {
503 // Client is idle - send a ping to check if it's still alive
504 worker_is_busy(WORKERS_WEBSOCKET_SEND_PING);
505 websocket_protocol_send_ping(wsc, NULL, 0);
506
507 // If no activity for over WS_INACTIVITY_TIMEOUT seconds, consider it dead
508 if(now - wsc->last_activity_t > WS_INACTIVITY_TIMEOUT) {
509 worker_is_busy(WORKERS_WEBSOCKET_CLIENT_TIMEOUT);
510 websocket_error(wsc, "Client timed out (no activity for over %d minutes)", WS_INACTIVITY_TIMEOUT / 60);
511 websocket_protocol_exception(wsc, WS_CLOSE_GOING_AWAY, "Timeout - no activity");
512 }
513 }
514 // For normal clients, send periodic pings (every WS_PERIODIC_PING_INTERVAL seconds)
515 else if(now - wsc->last_activity_t > WS_PERIODIC_PING_INTERVAL) {
516 worker_is_busy(WORKERS_WEBSOCKET_SEND_PING);
517 websocket_protocol_send_ping(wsc, NULL, 0);
518 }
519 }
520 else if(wsc->state == WS_STATE_CLOSING_SERVER || wsc->state == WS_STATE_CLOSING_CLIENT) {
521 // If a client is in any CLOSING state for more than WS_CLOSING_STATE_TIMEOUT seconds, force close it
522 if(now - wsc->last_activity_t > WS_CLOSING_STATE_TIMEOUT) {
523 worker_is_busy(WORKERS_WEBSOCKET_CLIENT_STUCK);
524 websocket_error(wsc, "Forcing close (stuck in %s state)",
525 wsc->state == WS_STATE_CLOSING_SERVER ? "CLOSING_SERVER" : "CLOSING_CLIENT");
526 websocket_thread_send_command(wth, WEBSOCKET_THREAD_CMD_REMOVE_CLIENT, wsc->id);
527 }
528 }
529
530 wsc = next;
531 }
532
533 spinlock_unlock(&wth->clients_spinlock);
534
535 last_cleanup = now;
536 }
537 }
538
539 netdata_log_info("WEBSOCKET[%zu] exiting", wth->id);
540
541 // Clean up any remaining clients
542 spinlock_lock(&wth->clients_spinlock);
543
544 // Close all clients in this thread
545 WS_CLIENT *wsc = wth->clients;
546
547 // SHUTDOWN MODE: Fast cleanup with timeouts to avoid blocking
548 usec_t cleanup_start = now_monotonic_usec();
549 usec_t max_cleanup_time = 5 * USEC_PER_SEC;
550 size_t clients_closed = 0, clients_skipped = 0;
551
552 while(wsc) {
553 WS_CLIENT *next = wsc->next;
554
555 // Cache elapsed time to avoid repeated syscalls
556 usec_t elapsed = now_monotonic_usec() - cleanup_start;
557
558 if(elapsed < max_cleanup_time && wsc->sock.fd >= 0) {
559 bool skip_client = false;
560
561 // Ensure socket is non-blocking
562 int flags = fcntl(wsc->sock.fd, F_GETFL, 0);
563 if(flags >= 0 && !(flags & O_NONBLOCK)) {
564 if(fcntl(wsc->sock.fd, F_SETFL, flags | O_NONBLOCK) < 0) {
565 websocket_debug(wsc, "Failed to set O_NONBLOCK during shutdown: %s", strerror(errno));
566 skip_client = true;
567 }
568 }
569
570 if(!skip_client) {
571 // Set send timeout: 100ms max per client
572 struct timeval timeout = { .tv_sec = 0, .tv_usec = 100000 };
573 if(setsockopt(wsc->sock.fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) {
574 websocket_debug(wsc, "Failed to set SO_SNDTIMEO during shutdown: %s (continuing)", strerror(errno));
575 // Non-critical, continue anyway
576 }
577
578 // Try to send close frame (won't block due to O_NONBLOCK + SO_SNDTIMEO)
579 websocket_protocol_send_close(wsc, WS_CLOSE_GOING_AWAY, "Server shutting down");
580 ssize_t written = websocket_write_data(wsc);
581 if(written < 0) {
582 websocket_debug(wsc, "Failed to send close frame during shutdown");
583 }
584 clients_closed++;
585 }
586 else {
587 clients_skipped++;
588 }
589 }
590 else {
591 clients_skipped++;
592 }
593
594 websocket_thread_remove_client(wth, wsc, true);
595 wsc = next;
596 }
597
598 netdata_log_info("WEBSOCKET[%zu] shutdown cleanup complete: %zu clients closed gracefully, %zu skipped",
599 wth->id, clients_closed, clients_skipped);
600
601 if(clients_skipped > 0 && clients_skipped > clients_closed) {
602 nd_log_daemon(NDLP_WARNING, "WEBSOCKET[%zu] skipped more clients (%zu) than closed gracefully (%zu) - "
603 "possible network issues or timeout reached",
604 wth->id, clients_skipped, clients_closed);
605 }
606
607 // Reset thread's client list
608 wth->clients = NULL;
609 wth->clients_current = 0;
610
611 spinlock_unlock(&wth->clients_spinlock);
612
613 // Cleanup poll resources
614 if(wth->ndpl) {
615 nd_poll_destroy(wth->ndpl);
616 wth->ndpl = NULL;
617 }
618
619 // Cleanup command pipe
620 if(wth->cmd.pipe[PIPE_READ] != -1) {
621 close(wth->cmd.pipe[PIPE_READ]);
622 wth->cmd.pipe[PIPE_READ] = -1;
623 }
624
625 if(wth->cmd.pipe[PIPE_WRITE] != -1) {
626 close(wth->cmd.pipe[PIPE_WRITE]);
627 wth->cmd.pipe[PIPE_WRITE] = -1;
628 }
629
630 freez(wth->cmd.buffer);
631 wth->cmd.buffer = NULL;
632 wth->cmd.buffer_size = 0;
633
634 // Mark thread as not running
635 spinlock_lock(&wth->spinlock);
636 wth->running = false;
637 spinlock_unlock(&wth->spinlock);
638 }