Add Bounded Web Admission Diagnostics
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Post-TLS observation only. Never retain request data or replace TLS cleanup. */
|
||||
#include "web_diagnostics.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user