Files
ESP32_Serial_Swiss_Army_Knife/src/web_diagnostics.c
T

234 lines
9.9 KiB
C

/* 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;
}