/* SPDX-License-Identifier: GPL-3.0-only */ /* Post-TLS observation only. Never retain request data or replace TLS cleanup. */ #include "web_diagnostics.h" #include #include #include #include #include "esp_heap_caps.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #define DIAG_SOCKETS 6U #define DIAG_EVENTS 32U typedef struct { const esp_tls_t *tls; /* Identity only, never dereferenced or printed. */ uint64_t seq; int64_t opened_us; int fd; unsigned kind; /* 0 ordinary, 1 serial WS, 2 admin WS */ } connection_t; typedef struct { uint64_t id, seq; int64_t at_us, elapsed_us; uint32_t free_bytes[3], largest[3], stack_bytes; int fd, result; unsigned event, route, ordinary, serial, admin; } trace_t; enum { TLS_OPEN, TLS_CLOSE, ENTER, RESULT }; static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED; static connection_t s_connections[DIAG_SOCKETS]; static trace_t s_events[DIAG_EVENTS]; static bool s_enabled; static uint64_t s_connection_seq, s_event_seq, s_epoch; static uint32_t s_overwritten, s_unmatched, s_lost; static unsigned s_count, s_next; static void increment(uint32_t *value) { if (*value != UINT32_MAX) ++*value; } static void occupancy(trace_t *event) { for (unsigned i = 0; i < DIAG_SOCKETS; ++i) { if (!s_connections[i].seq) continue; if (s_connections[i].kind == 1) ++event->serial; else if (s_connections[i].kind == 2) ++event->admin; else ++event->ordinary; } } /* Called by HTTPD only. Expensive capability scans stay outside the portMUX. * Epoch rejects a sample crossing clear/disable/enable. Sequence fences fd reuse. */ static void record(connection_t connection, unsigned event, unsigned route, int result, int64_t elapsed_us, uint64_t epoch) { portENTER_CRITICAL(&s_lock); bool enabled = s_enabled && s_epoch == epoch; portEXIT_CRITICAL(&s_lock); if (!enabled) return; trace_t row = {.seq = connection.seq, .fd = connection.fd, .at_us = esp_timer_get_time(), .elapsed_us = elapsed_us, .event = event, .route = route, .result = result}; const uint32_t caps[] = {MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT}; for (unsigned i = 0; i < 3; ++i) { row.free_bytes[i] = heap_caps_get_free_size(caps[i]); row.largest[i] = heap_caps_get_largest_free_block(caps[i]); } /* ESP-IDF FreeRTOS reports minimum-free stack in bytes, not vanilla words. */ row.stack_bytes = uxTaskGetStackHighWaterMark(NULL); portENTER_CRITICAL(&s_lock); if (s_enabled && s_epoch == epoch && s_event_seq != UINT64_MAX) { row.id = ++s_event_seq; occupancy(&row); s_events[s_next] = row; s_next = (s_next + 1U) % DIAG_EVENTS; if (s_count < DIAG_EVENTS) ++s_count; else increment(&s_overwritten); } portEXIT_CRITICAL(&s_lock); } void web_diagnostics_tls(esp_https_server_user_cb_arg_t *arg) { if (!arg || !arg->tls) return; connection_t connection = {0}; int fd = -1; if (arg->user_cb_state == HTTPD_SSL_USER_CB_SESS_CREATE && (esp_tls_get_conn_sockfd(arg->tls, &fd) != ESP_OK || fd < 0)) { portENTER_CRITICAL(&s_lock); increment(&s_lost); portEXIT_CRITICAL(&s_lock); return; } int64_t now = esp_timer_get_time(); portENTER_CRITICAL(&s_lock); uint64_t epoch = s_epoch; if (arg->user_cb_state == HTTPD_SSL_USER_CB_SESS_CREATE) { unsigned i; for (i = 0; i < DIAG_SOCKETS; ++i) if (s_connections[i].seq && (s_connections[i].tls == arg->tls || s_connections[i].fd == fd)) break; /* Duplicate notifications are not new connections. */ if (i != DIAG_SOCKETS) { increment(&s_unmatched); } else { for (i = 0; i < DIAG_SOCKETS; ++i) if (!s_connections[i].seq) break; if (i < DIAG_SOCKETS && s_connection_seq != UINT64_MAX) { connection = (connection_t){.tls = arg->tls, .fd = fd, .seq = ++s_connection_seq, .opened_us = now}; s_connections[i] = connection; } else increment(&s_lost); } } else if (arg->user_cb_state == HTTPD_SSL_USER_CB_SESS_CLOSE) { unsigned i; for (i = 0; i < DIAG_SOCKETS; ++i) if (s_connections[i].seq && s_connections[i].tls == arg->tls) break; if (i < DIAG_SOCKETS) { connection = s_connections[i]; memset(&s_connections[i], 0, sizeof(s_connections[i])); } else increment(&s_unmatched); } portEXIT_CRITICAL(&s_lock); if (connection.seq) record(connection, arg->user_cb_state == HTTPD_SSL_USER_CB_SESS_CREATE ? TLS_OPEN : TLS_CLOSE, 0, 0, now - connection.opened_us, epoch); } esp_err_t web_diagnostics_handler(httpd_req_t *request, web_diag_route_t route, esp_err_t (*handler)(httpd_req_t *)) { int fd = httpd_req_to_sockfd(request); connection_t connection = {.fd = fd}; portENTER_CRITICAL(&s_lock); uint64_t epoch = s_epoch; bool enabled = s_enabled; for (unsigned i = 0; i < DIAG_SOCKETS; ++i) if (s_connections[i].seq && s_connections[i].fd == fd) connection = s_connections[i]; portEXIT_CRITICAL(&s_lock); record(connection, ENTER, route, 0, 0, epoch); int64_t start = enabled ? esp_timer_get_time() : 0; esp_err_t result = handler(request); int64_t elapsed = enabled ? esp_timer_get_time() - start : 0; if (route == WEB_DIAG_SERIAL_UPGRADE || route == WEB_DIAG_ADMIN_UPGRADE) { bool upgraded = httpd_ws_get_fd_info(request->handle, fd) == HTTPD_WS_CLIENT_WEBSOCKET; portENTER_CRITICAL(&s_lock); for (unsigned i = 0; i < DIAG_SOCKETS; ++i) if (connection.seq && s_connections[i].seq == connection.seq && upgraded) s_connections[i].kind = route == WEB_DIAG_SERIAL_UPGRADE ? 1U : 2U; portEXIT_CRITICAL(&s_lock); } record(connection, RESULT, route, result, elapsed, epoch); return result; } static void show(void) { connection_t connections[DIAG_SOCKETS]; trace_t counts = {0}; portENTER_CRITICAL(&s_lock); memcpy(connections, s_connections, sizeof(connections)); occupancy(&counts); uint64_t last = s_event_seq; unsigned count = s_count; bool enabled = s_enabled; uint32_t overwritten = s_overwritten, unmatched = s_unmatched, lost = s_lost; portEXIT_CRITICAL(&s_lock); int64_t now = esp_timer_get_time(); printf("Web diagnostics %s; post-TLS occupancy=%u/6 ordinary=%u serial=%u admin=%u\n", enabled ? "enabled" : "disabled", counts.ordinary + counts.serial + counts.admin, counts.ordinary, counts.serial, counts.admin); printf("snapshot_us=%" PRId64 " retained=%u/32 overwritten=%" PRIu32 " unmatched=%" PRIu32 " lost=%" PRIu32 "\n", now, count, overwritten, unmatched, lost); printf("No preaccept/TLS-failure timing; occupancy excludes in-progress TLS. rc is handler return, NOT HTTP status.\n"); for (unsigned i = 0; i < DIAG_SOCKETS; ++i) { connection_t c = connections[i]; if (c.seq) printf("live fd=%d conn=%" PRIu64 " kind=%u opened_us=%" PRId64 " age_us=%" PRId64 "\n", c.fd, c.seq, c.kind, c.opened_us, now - c.opened_us); } printf("events: open/close/enter/result; routes: serial-ticket/admin-ticket/serial-upgrade/admin-upgrade; heap pairs free/largest internal,DMA,PSRAM bytes; stack=HTTPD minimum-free bytes\n"); const char *const events[] = {"open", "close", "enter", "result"}; const char *const routes[] = {"serial-ticket", "admin-ticket", "serial-upgrade", "admin-upgrade"}; /* Copy one immutable-ID-qualified row at a time; never hold a lock while printing. * Concurrent overwrite/clear can omit rows, but cannot turn show into an endless stream. */ for (unsigned n = 0; n < count; ++n) { uint64_t id = last - count + 1U + n; trace_t row = {0}; portENTER_CRITICAL(&s_lock); for (unsigned i = 0; i < DIAG_EVENTS; ++i) if (s_events[i].id == id) { row = s_events[i]; break; } portEXIT_CRITICAL(&s_lock); if (!row.id) { printf("event=%" PRIu64 " no longer retained\n", id); continue; } printf("event=%" PRIu64 " t_us=%" PRId64 " fd=%d conn=%" PRIu64 " %s %s rc=%d dt_us=%" PRId64 " occ=%u/%u/%u heap=%" PRIu32 "/%" PRIu32 ",%" PRIu32 "/%" PRIu32 ",%" PRIu32 "/%" PRIu32 " stack=%" PRIu32 "\n", row.id, row.at_us, row.fd, row.seq, events[row.event], row.event < ENTER ? "-" : routes[row.route], row.result, row.elapsed_us, row.ordinary, row.serial, row.admin, row.free_bytes[0], row.largest[0], row.free_bytes[1], row.largest[1], row.free_bytes[2], row.largest[2], row.stack_bytes); } } int web_diagnostics_command(const char *action) { if (strcmp(action, "show") == 0) { show(); return 0; } bool enable = strcmp(action, "enable") == 0; bool disable = strcmp(action, "disable") == 0; bool clear = strcmp(action, "clear") == 0; if (!enable && !disable && !clear) return 1; portENTER_CRITICAL(&s_lock); /* Never wrap identity or capture epochs; exhausting diagnostics cannot affect HTTPD. */ if (s_epoch != UINT64_MAX) { ++s_epoch; if (enable || disable) s_enabled = enable; } else s_enabled = false; if (clear) { memset(s_events, 0, sizeof(s_events)); s_count = s_next = 0; s_overwritten = s_unmatched = s_lost = 0; } bool enabled = s_enabled; portEXIT_CRITICAL(&s_lock); printf("Web diagnostics %s%s; live identities retained.\n", enabled ? "enabled" : "disabled", clear ? ", trace cleared" : ""); return 0; }