Add admin serial settings view

This commit is contained in:
2026-09-07 20:12:33 +02:00
parent c73674cda2
commit 5a2aa0d4d8
20 changed files with 612 additions and 25 deletions
+33 -10
View File
@@ -37,8 +37,8 @@ def define(path, name):
uri_tables = re.findall(r'^static const httpd_uri_t(?: \*const)? \w+\[?\]? = \{.*?^\};',
source, re.M | re.S)
# Non-array declarations have no brackets; explicit shape avoids silent omission.
if len(uri_tables) != 13:
raise RuntimeError('Review URI extraction: expected 11 descriptors and two tables')
if len(uri_tables) != 14:
raise RuntimeError('Review URI extraction: expected 12 descriptors and two tables')
state = source[source.index('static SemaphoreHandle_t s_server_mutex;'):
source.index('static esp_err_t ensure_mutex(void)')]
header = (ROOT / 'src/web_server.h').read_text()
@@ -93,6 +93,8 @@ static unsigned ssl_starts, ssl_stops, serial_attaches, serial_detaches;
static unsigned admin_attaches, admin_detaches, admin_stoppeds;
static unsigned registration_calls, registration_fail_at, registered_count, unregister_calls;
static bool unregister_fail;
static bool settings_fail;
static unsigned settings_calls;
static const httpd_uri_t *registered[32];
static char events[128]; static size_t event_length;
static void event(char value) { assert(!locked && event_length + 1 < sizeof(events)); events[event_length++] = value; events[event_length] = 0; }
@@ -104,6 +106,7 @@ static void secure_wipe(void *p, size_t n) { assert(!locked); memset(p, 0, n); }
HANDLER(root_handler) HANDLER(status_handler) HANDLER(ticket_handler)
HANDLER(websocket_handler) HANDLER(asset_handler) HANDLER(web_cookie_auth_handler)
HANDLER(web_admin_transport_ticket_handler) HANDLER(web_admin_transport_upgrade_handler)
HANDLER(serial_settings_handler)
static esp_err_t route_error_handler(httpd_req_t *r, httpd_err_code_t c) { (void)r; (void)c; assert(0); return ESP_FAIL; }
static esp_err_t web_serial_transport_init(void) { assert(!locked); ++serial_inits; return serial_init_error; }
static esp_err_t web_cookie_auth_start(void) { assert(!locked); ++auth_starts; auth_live = auth_error == ESP_OK; return auth_error; }
@@ -115,7 +118,7 @@ static esp_err_t web_security_copy_tls_material(uint8_t *cert, size_t nc, size_t
static esp_err_t httpd_ssl_start(httpd_handle_t *server, const httpd_ssl_config_t *config) {
assert(!locked && auth_live && !ssl_live); ++ssl_starts;
assert(config->httpd.max_open_sockets == 6 && !config->httpd.lru_purge_enable);
assert(config->httpd.max_uri_handlers == 16 && config->port_secure == 443);
assert(config->httpd.max_uri_handlers == 17 && config->port_secure == 443);
assert(config->httpd.recv_wait_timeout == 1 && config->httpd.send_wait_timeout == 1);
assert(config->tls_handshake_timeout_ms == 5000);
assert(config->servercert_len == 1 && config->servercert[0] == 1);
@@ -128,6 +131,14 @@ static esp_err_t register_one(httpd_handle_t server) {
return registration_calls == registration_fail_at ? ESP_FAIL : ESP_OK;
}
static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t *uri) {
if (!strcmp(uri->uri, "/api/settings/serial")) {
assert(s == SERVER && auth_live && ssl_live && registration_calls >= 17);
assert(uri->method == HTTP_GET && uri->handler == serial_settings_handler);
++settings_calls;
if (settings_fail) return ESP_ERR_NO_MEM;
registered[registered_count++] = uri;
return ESP_OK;
}
if (!strcmp(uri->uri, "/api/admin/ws-ticket") || !strcmp(uri->uri, "/ws/admin")) {
assert(registration_calls >= 16);
assert(serial_init_error != ESP_OK || serial_live);
@@ -136,6 +147,10 @@ static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t
if (error == ESP_OK) { assert(registered_count < 32); registered[registered_count++] = uri; }
return error;
}
static esp_err_t web_httpd_register_optional_get(httpd_handle_t s, const httpd_uri_t *uri) {
assert(!strcmp(uri->uri, "/api/settings/serial"));
return httpd_register_uri_handler(s, uri);
}
static esp_err_t httpd_unregister_uri_handler(httpd_handle_t s, const char *uri, int method) {
assert(!locked && s == SERVER && ssl_live && auth_live && serial_live);
assert(registration_calls == 18 && !strcmp(uri, "/api/admin/ws-ticket") && method == HTTP_POST);
@@ -201,7 +216,7 @@ static void reset(void) {
serial_inits = admin_inits = auth_starts = auth_stops = ssl_starts = ssl_stops = 0;
serial_attaches = serial_detaches = admin_attaches = admin_detaches = admin_stoppeds = 0;
registration_calls = registration_fail_at = registered_count = unregister_calls = 0;
unregister_fail = false; clear_events();
unregister_fail = settings_fail = false; settings_calls = 0; clear_events();
}
static void fresh_registration(void) { registration_calls = registered_count = 0; }
static void start(void) {
@@ -235,7 +250,8 @@ int main(void) {
}
puts("PASS optional admin init/attach failures do not disable M1 auth or serial attachment");
reset(); start(); assert(registered_count == 16 && registration_calls == 18);
reset(); start(); assert(registered_count == 17 && registration_calls == 18 && settings_calls == 1);
assert(route("/api/settings/serial")->handler == serial_settings_handler);
const httpd_uri_t *ticket = route("/api/admin/ws-ticket"), *ws = route("/ws/admin");
assert(ticket->method == HTTP_POST && ticket->handler == web_admin_transport_ticket_handler && !ticket->is_websocket);
assert(ws->method == HTTP_GET && ws->handler == web_admin_transport_upgrade_handler && !ws->is_websocket);
@@ -286,7 +302,7 @@ int main(void) {
assert(s_serial_transport_attached && !s_admin_transport_owned && !admin_owned);
assert(!admin_inits && !admin_attaches && !auth_stops && !ssl_stops);
assert(!s_transitioning && s_last_error == ESP_OK && s_counters.starts == 1 && !s_counters.start_failures);
assert(registered_count == 14 && unregister_calls == failure - 17);
assert(registered_count == 15 && unregister_calls == failure - 17);
for (unsigned i = 0; i < registered_count; ++i)
assert(strcmp(registered[i]->uri, "/api/admin/ws-ticket") && strcmp(registered[i]->uri, "/ws/admin"));
assert(route("/ws/serial")->handler == websocket_handler);
@@ -295,13 +311,13 @@ int main(void) {
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
assert(!admin_detaches && !admin_stoppeds);
registration_fail_at = 0; fresh_registration(); start();
assert(registered_count == 16 && admin_attaches == 1 && s_counters.starts == 2);
assert(registered_count == 17 && admin_attaches == 1 && s_counters.starts == 2);
assert(web_server_stop() == ESP_OK && admin_stoppeds == 1);
}
puts("PASS optional positions 17..18 preserve M1, roll back ticket when needed and recover after stop/restart");
reset(); registration_fail_at = 18; unregister_fail = true;
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 15);
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 16);
assert(auth_live && ssl_live && serial_live && s_serial_transport_attached);
assert(!admin_inits && !admin_attaches && !admin_owned && !s_admin_transport_owned);
ticket = route("/api/admin/ws-ticket");
@@ -313,7 +329,7 @@ int main(void) {
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
assert(!admin_detaches && !admin_stoppeds);
unregister_fail = false; registration_fail_at = 0; fresh_registration(); start();
assert(registered_count == 16 && admin_attaches == 1 && web_server_stop() == ESP_OK);
assert(registered_count == 17 && admin_attaches == 1 && web_server_stop() == ESP_OK);
puts("PASS failed unregister retains only original ticket handler, no admin attachment, and permits restart");
reset(); registration_fail_at = 6; ssl_stop_error = ESP_FAIL;
@@ -334,7 +350,14 @@ int main(void) {
assert(web_server_start() == ESP_ERR_INVALID_STATE && !auth_starts);
assert(web_server_stop() == ESP_ERR_INVALID_STATE && !auth_stops);
puts("PASS auth/start failure gates and invalid/transitioning lifecycle rejection");
puts("11 lifecycle groups passed (16 required fatal positions, 2 optional positions, plus failed unregister)");
reset(); settings_fail = true; start();
assert(settings_calls == 1 && registered_count == 16);
assert(auth_live && serial_live && admin_owned && web_server_stop() == ESP_OK);
settings_fail = false; fresh_registration(); start();
assert(route("/api/settings/serial")->handler == serial_settings_handler);
assert(web_server_stop() == ESP_OK);
puts("PASS optional Settings registration failure preserves auth and both transports; restart recovers");
puts("12 lifecycle groups passed (16 required fatal positions, 3 optional routes, plus failed unregister)");
return 0;
}
'''