Add Bounded Ordinary HTTPS Idle Cleanup
This commit is contained in:
@@ -108,7 +108,28 @@ static void secure_wipe(void *p, size_t n) { assert(!locked); memset(p, 0, n); }
|
||||
HANDLER(root_handler) HANDLER(status_handler) HANDLER(traced_ticket_handler)
|
||||
HANDLER(traced_websocket_handler) HANDLER(asset_handler) HANDLER(web_cookie_auth_handler)
|
||||
HANDLER(traced_admin_ticket_handler) HANDLER(traced_admin_upgrade_handler)
|
||||
static void web_diagnostics_tls(void *arg) { (void)arg; assert(false); }
|
||||
typedef void esp_https_server_user_cb_arg_t;
|
||||
static unsigned tls_steps;
|
||||
static void web_httpd_idle_tls(void *arg) { assert(arg && tls_steps == 0); ++tls_steps; }
|
||||
static void web_diagnostics_tls(void *arg) { assert(arg && tls_steps == 1); ++tls_steps; }
|
||||
static void tls_session_callback(esp_https_server_user_cb_arg_t *arg);
|
||||
static bool idle_owned, idle_fenced;
|
||||
static esp_err_t idle_prepare_error, idle_attach_error, idle_detach_error;
|
||||
static unsigned idle_prepares, idle_attaches, idle_detaches, idle_stoppeds;
|
||||
static esp_err_t web_httpd_idle_prepare(void) {
|
||||
assert(!locked && !ssl_live && !idle_owned); ++idle_prepares; return idle_prepare_error;
|
||||
}
|
||||
static esp_err_t web_httpd_idle_attach(httpd_handle_t s) {
|
||||
assert(!locked && s == SERVER && ssl_live && !idle_owned); ++idle_attaches;
|
||||
idle_owned = idle_attach_error == ESP_OK; idle_fenced = false; return idle_attach_error;
|
||||
}
|
||||
static esp_err_t web_httpd_idle_detach(httpd_handle_t s) {
|
||||
assert(!locked && s == SERVER && ssl_live && !auth_live); ++idle_detaches;
|
||||
idle_fenced = idle_detach_error == ESP_OK; return idle_detach_error;
|
||||
}
|
||||
static void web_httpd_idle_stopped(httpd_handle_t s) {
|
||||
assert(!locked && s == SERVER && !ssl_live && idle_fenced); ++idle_stoppeds; idle_owned = false;
|
||||
}
|
||||
HANDLER(serial_settings_handler)
|
||||
HANDLER(web_serial_settings_handler) HANDLER(web_account_settings_handler)
|
||||
HANDLER(web_account_generate_password_handler) HANDLER(web_account_keys_handler)
|
||||
@@ -131,7 +152,8 @@ static esp_err_t httpd_ssl_start(httpd_handle_t *server, const httpd_ssl_config_
|
||||
assert(config->httpd.max_uri_handlers == 24 && 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->user_cb == web_diagnostics_tls);
|
||||
assert(config->user_cb == tls_session_callback);
|
||||
tls_steps = 0; config->user_cb(&tls_steps); assert(tls_steps == 2);
|
||||
assert(config->servercert_len == 1 && config->servercert[0] == 1);
|
||||
assert(config->prvtkey_len == 1 && config->prvtkey_pem[0] == 2);
|
||||
if (ssl_start_error != ESP_OK) return ssl_start_error;
|
||||
@@ -233,7 +255,7 @@ static esp_err_t web_serial_transport_detach_server(httpd_handle_t s) {
|
||||
event('S'); ++serial_detaches; serial_live = false; return ESP_OK;
|
||||
}
|
||||
static esp_err_t httpd_ssl_stop(httpd_handle_t s) {
|
||||
assert(s == SERVER && ssl_live && !auth_live); event('H'); ++ssl_stops;
|
||||
assert(s == SERVER && ssl_live && !auth_live && idle_fenced); event('H'); ++ssl_stops;
|
||||
if (ssl_stop_error == ESP_OK) ssl_live = false;
|
||||
return ssl_stop_error;
|
||||
}
|
||||
@@ -253,6 +275,9 @@ static void reset(void) {
|
||||
s_last_error = s_serial_transport_error = ESP_ERR_INVALID_STATE;
|
||||
memset(&s_counters, 0, sizeof(s_counters));
|
||||
mutex_fail = auth_live = ssl_live = admin_owned = serial_live = false;
|
||||
idle_owned = idle_fenced = false;
|
||||
idle_prepare_error = idle_attach_error = idle_detach_error = ESP_OK;
|
||||
idle_prepares = idle_attaches = idle_detaches = idle_stoppeds = 0;
|
||||
serial_init_error = admin_init_error = admin_attach_error = ESP_OK;
|
||||
auth_error = ssl_start_error = ssl_stop_error = admin_detach_error = ESP_OK;
|
||||
serial_inits = admin_inits = auth_starts = auth_stops = ssl_starts = ssl_stops = 0;
|
||||
@@ -267,7 +292,7 @@ static void fresh_registration(void) { registration_calls = registered_count = 0
|
||||
static void start(void) {
|
||||
assert(web_server_start() == ESP_OK);
|
||||
assert(s_server == SERVER && s_admin_transport_owned && s_serial_transport_attached);
|
||||
assert(auth_live && ssl_live && admin_owned && serial_live && !s_transitioning);
|
||||
assert(auth_live && ssl_live && admin_owned && serial_live && !s_transitioning && idle_owned);
|
||||
}
|
||||
static const httpd_uri_t *route(const char *uri) {
|
||||
const httpd_uri_t *found = NULL;
|
||||
@@ -468,12 +493,32 @@ int main(void) {
|
||||
assert(route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
puts("PASS optional account keys allocation failure preserves account/generation/auth/transports; restart recovers");
|
||||
puts("16 lifecycle groups passed (16 required fatal positions, 10 optional routes, plus failed unregister)");
|
||||
reset(); idle_prepare_error = ESP_ERR_NO_MEM;
|
||||
assert(web_server_start() == ESP_ERR_NO_MEM && !ssl_starts && !auth_live);
|
||||
assert(idle_prepares == 1 && !idle_attaches && !s_server);
|
||||
reset(); idle_attach_error = ESP_FAIL;
|
||||
assert(web_server_start() == ESP_FAIL && !ssl_live && !s_server);
|
||||
assert(idle_detaches == 1 && idle_stoppeds == 1 && !serial_attaches);
|
||||
puts("PASS idle timer preparation/attachment failures gate startup and clean partial HTTPD");
|
||||
reset(); start(); idle_detach_error = ESP_ERR_TIMEOUT;
|
||||
assert(web_server_stop() == ESP_ERR_TIMEOUT && ssl_live && idle_owned);
|
||||
assert(!ssl_stops && !admin_detaches && !serial_detaches && !idle_stoppeds);
|
||||
assert(!s_transitioning && s_server == SERVER && web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
idle_detach_error = ESP_OK; ssl_stop_error = ESP_FAIL;
|
||||
assert(web_server_stop() == ESP_FAIL && idle_owned && !idle_stoppeds);
|
||||
ssl_stop_error = ESP_OK;
|
||||
assert(web_server_stop() == ESP_OK && !idle_owned && idle_stoppeds == 1);
|
||||
fresh_registration(); start(); assert(web_server_stop() == ESP_OK);
|
||||
puts("PASS idle submit fence failure forbids SSL destruction; failed stop retains ownership until retry/restart");
|
||||
puts("18 lifecycle groups passed (16 required fatal positions, 10 optional routes, plus failed unregister)");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
unit = FAKES + header + '\n' + constants + state + '\n'.join(uri_tables)
|
||||
callback = re.search(r'^static void tls_session_callback\(.*?^\}', source, re.M | re.S)
|
||||
assert callback
|
||||
unit += callback.group() + '\n'
|
||||
unit += function('ensure_mutex')
|
||||
unit += ''.join(function(name) for name in ('web_server_init', 'web_server_start', 'web_server_stop'))
|
||||
unit += TESTS
|
||||
|
||||
@@ -31,13 +31,18 @@ HEADERS["esp_idf_version.h"] = """
|
||||
#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(5,5,0)
|
||||
"""
|
||||
HEADERS["esp_httpd_priv.h"] = """#pragma once
|
||||
#include <stdint.h>
|
||||
#include "esp_http_server.h"
|
||||
struct sock_db { bool ws_handshake_done; esp_err_t (*ws_handler)(httpd_req_t *);
|
||||
static inline void *httpd_os_thread_handle(void) { return (void *)1; }
|
||||
struct sock_db { int fd; bool for_async_req, ws_close; uint64_t lru_counter;
|
||||
int (*pending_fn)(httpd_handle_t, int);
|
||||
bool ws_handshake_done; esp_err_t (*ws_handler)(httpd_req_t *);
|
||||
bool ws_control_frames; void *ws_user_ctx; char pending_data[128]; size_t pending_len; };
|
||||
struct httpd_req_aux { struct sock_db *sd; char *scratch; size_t scratch_cur_size, remaining_len;
|
||||
unsigned req_hdrs_count, resp_hdrs_count; bool ws_handshake_detect;
|
||||
struct resp_hdr { const char *field, *value; } *resp_hdrs; };
|
||||
struct httpd_data { struct { unsigned max_resp_headers, max_uri_handlers; void *uri_match_fn; } config;
|
||||
struct httpd_data { struct { unsigned max_resp_headers, max_uri_handlers, max_open_sockets; void *uri_match_fn; } config;
|
||||
struct { void *handle; } hd_td; struct httpd_req_aux hd_req_aux; struct sock_db *hd_sd;
|
||||
httpd_uri_t **hd_calls; };
|
||||
esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *, const char *);
|
||||
"""
|
||||
|
||||
@@ -24,8 +24,10 @@ for name in ('traced_ticket_handler', 'traced_websocket_handler',
|
||||
match = re.search(r'static esp_err_t ' + name + r'\(httpd_req_t \*request\)\n\{.*?\n\}', server, re.S)
|
||||
assert match, name
|
||||
wrappers.append(match.group())
|
||||
# This slice must not take over cleanup, add async probes, or enable SDK logging.
|
||||
assert 'config.user_cb = web_diagnostics_tls;' in server
|
||||
# Diagnostics itself must remain observation-only. The server composes the
|
||||
# independent, always-on idle policy before diagnostic publication.
|
||||
assert 'config.user_cb = tls_session_callback;' in server
|
||||
assert 'web_httpd_idle_tls(arg);\n web_diagnostics_tls(arg);' in server
|
||||
assert 'config.httpd.close_fn' not in server and 'config.httpd.open_fn' not in server
|
||||
source = (ROOT / 'src/web_diagnostics.c').read_text()
|
||||
for forbidden in ('httpd_queue_work', 'httpd_get_client_list', 'esp_event_handler_register',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ordinary HTTPS idle cleanup regressions
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
python3 tests/web_httpd_idle/run.py
|
||||
python3 tests/web_admin_transport/server_lifecycle.py
|
||||
python3 tests/web_diagnostics/run.py
|
||||
```
|
||||
|
||||
Requires a C11 host compiler (`CC`, default `cc`) and the installed ESP-IDF 5.5.0 source (`IDF_PATH`, default `~/.platformio/packages/framework-espidf`). No packages are installed; binaries live in a temporary directory. No network, device or firmware operation.
|
||||
|
||||
`run.py` compiles the complete production idle lifecycle module and the production adapter sweep with installed SDK request-completion/body-purge functions. `fakes.h`/`test.c` provide deterministic TLS/parser/response/timer/queue/session-deletion doubles. Real local POSIX sockets exercise readability, shutdown, peer EOF and numeric fd reuse.
|
||||
|
||||
18 groups cover all six slots, normal five-second polling, never-used post-TLS connections, buffered/pipelined/incomplete input, slow synchronous processing/purge, failed requests, WS/upgrade/async exemptions, fd/TLS/slot/LRU reuse, shutdown retry, queue failure/loss, bounded submission, early callback completion, stop fences, failed stop/restart and stale/nonwrapping generations. The module is tested without linking diagnostics, and source guards enforce independent policy plus composed TLS callback ordering; the diagnostic and server suites exercise their own integration contracts.
|
||||
|
||||
These are not real TLS, ESP-IDF scheduler, TCP-over-network or target timing tests. In particular, accepted-but-lost UDP work is tested as a bounded fail-safe condition requiring successful server restart, not as automatic recovery. Full timeout/SDK audit, resource evidence and unperformed hardware checklist: [`docs/https_idle_cleanup.md`](../../docs/https_idle_cleanup.md).
|
||||
@@ -0,0 +1,84 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
typedef int esp_err_t;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE,
|
||||
ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT, ESP_ERR_NOT_SUPPORTED };
|
||||
typedef void *httpd_handle_t;
|
||||
typedef struct { int unused; } httpd_uri_t;
|
||||
struct httpd_req_aux;
|
||||
typedef struct { httpd_handle_t handle; struct httpd_req_aux *aux; } httpd_req_t;
|
||||
struct sock_db {
|
||||
int fd; uint64_t lru_counter; bool for_async_req, ws_handshake_done, ws_close;
|
||||
size_t pending_len; int (*pending_fn)(httpd_handle_t, int);
|
||||
};
|
||||
struct httpd_req_aux { struct sock_db *sd; size_t remaining_len; };
|
||||
struct httpd_data {
|
||||
struct { unsigned max_open_sockets; } config;
|
||||
struct { void *handle; } hd_td;
|
||||
struct sock_db *hd_sd;
|
||||
struct httpd_req_aux hd_req_aux;
|
||||
httpd_req_t hd_req;
|
||||
uint64_t lru_counter;
|
||||
};
|
||||
static bool owner;
|
||||
static unsigned lock_depth, shutdown_calls, select_calls;
|
||||
static int shutdown_error, select_error, tls_pending[FD_SETSIZE];
|
||||
static int64_t now_us;
|
||||
static void *httpd_os_thread_handle(void) { return owner ? (void *)1 : (void *)2; }
|
||||
typedef int portMUX_TYPE;
|
||||
#define portMUX_INITIALIZER_UNLOCKED 0
|
||||
#define taskENTER_CRITICAL(lock) do { (void)(lock); assert(!lock_depth); ++lock_depth; } while (0)
|
||||
#define taskEXIT_CRITICAL(lock) do { (void)(lock); assert(lock_depth == 1); --lock_depth; } while (0)
|
||||
static int64_t esp_timer_get_time(void) { assert(!lock_depth); return now_us; }
|
||||
static void (*delay_hook)(void);
|
||||
static void vTaskDelay(int ticks) { assert(!lock_depth && ticks == 1); now_us += 100000; if (delay_hook) delay_hook(); }
|
||||
typedef void *esp_timer_handle_t;
|
||||
typedef struct { void (*callback)(void *); const char *name; bool skip_unhandled_events; } esp_timer_create_args_t;
|
||||
static esp_err_t timer_create_error, timer_start_error, queue_error;
|
||||
static unsigned timer_creates, timer_starts, timer_deletes, queue_calls;
|
||||
static void (*timer_callback)(void *), (*queued_work)(void *);
|
||||
static void *queued_arg;
|
||||
static void (*queue_hook)(void);
|
||||
static bool inline_work;
|
||||
static esp_err_t esp_timer_create(const esp_timer_create_args_t *args, esp_timer_handle_t *timer) {
|
||||
assert(!lock_depth && args->skip_unhandled_events); ++timer_creates;
|
||||
if (timer_create_error) return timer_create_error;
|
||||
timer_callback = args->callback; *timer = (void *)3; return ESP_OK;
|
||||
}
|
||||
static esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, int64_t period) {
|
||||
assert(!lock_depth && timer && period == 1000000); ++timer_starts; return timer_start_error;
|
||||
}
|
||||
static esp_err_t esp_timer_delete(esp_timer_handle_t timer) { assert(!lock_depth && timer); ++timer_deletes; return ESP_OK; }
|
||||
static esp_err_t httpd_queue_work(httpd_handle_t, void (*)(void *), void *);
|
||||
typedef struct { int fd; bool get_error; } esp_tls_t;
|
||||
enum { HTTPD_SSL_USER_CB_SESS_CREATE, HTTPD_SSL_USER_CB_SESS_CLOSE };
|
||||
typedef struct { esp_tls_t *tls; unsigned user_cb_state; } esp_https_server_user_cb_arg_t;
|
||||
static esp_err_t esp_tls_get_conn_sockfd(esp_tls_t *tls, int *fd) {
|
||||
assert(owner && !lock_depth); *fd = tls->fd; return tls->get_error ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
static int test_select(int n, fd_set *r, fd_set *w, fd_set *e, struct timeval *t) {
|
||||
assert(owner && !lock_depth && !w && !e && t && !t->tv_sec && !t->tv_usec);
|
||||
++select_calls; return select_error ? -1 : select(n, r, w, e, t);
|
||||
}
|
||||
static int test_shutdown(int fd, int how) {
|
||||
assert(owner && !lock_depth && how == SHUT_RDWR); ++shutdown_calls;
|
||||
return shutdown_error ? -1 : shutdown(fd, how);
|
||||
}
|
||||
#define select test_select
|
||||
#define shutdown test_shutdown
|
||||
#define CONFIG_HTTPD_PURGE_BUF_LEN 32
|
||||
#define ESP_LOGD(...) ((void)0)
|
||||
#define ESP_LOG_BUFFER_HEX_LEVEL(...) ((void)0)
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
static esp_err_t httpd_req_new(struct httpd_data *, struct sock_db *);
|
||||
static int httpd_req_recv(httpd_req_t *, char *, size_t);
|
||||
static void httpd_req_cleanup(httpd_req_t *);
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Production idle lifecycle/sweep + installed IDF completion path, real host sockets.
|
||||
|
||||
Deterministic owner/queue/TLS doubles, not a TLS server or hardware timing test.
|
||||
No dependency installation, device operation, or SDK modification.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
IDF = Path(os.environ.get('IDF_PATH', str(Path.home() / '.platformio/packages/framework-espidf')))
|
||||
|
||||
|
||||
def stripped(path):
|
||||
return '\n'.join(line for line in path.read_text().splitlines()
|
||||
if not line.startswith(('#include', '#pragma once')))
|
||||
|
||||
|
||||
def function(source, name):
|
||||
match = re.search(r'^(?:static )?(?:void|esp_err_t) ' + name + r'\(.*?^\}', source, re.M | re.S)
|
||||
assert match, name
|
||||
return match.group() + '\n'
|
||||
|
||||
|
||||
sess = (IDF / 'components/esp_http_server/src/httpd_sess.c').read_text()
|
||||
parse = (IDF / 'components/esp_http_server/src/httpd_parse.c').read_text()
|
||||
main = (IDF / 'components/esp_http_server/src/httpd_main.c').read_text()
|
||||
ssl = (IDF / 'components/esp_https_server/src/https_server.c').read_text()
|
||||
completion = function(sess, 'httpd_sess_process')
|
||||
assert completion.index('httpd_req_new') < completion.index('httpd_req_delete') < completion.index('session->lru_counter = ++hd->lru_counter')
|
||||
assert 'Only listen for new connections if server has capacity' in main
|
||||
assert main.index('/* Case0:') < main.index('/* Case1:') < main.index('/* Case2:')
|
||||
assert 'httpd_sess_delete(hd, sock_db);' in sess # queued reusable-pointer close is not safe here
|
||||
assert ssl.index('httpd_sess_set_pending_override') < ssl.index('HTTPD_SSL_USER_CB_SESS_CREATE')
|
||||
adapter = (ROOT / 'src/web_httpd_adapter.c').read_text()
|
||||
idle = (ROOT / 'src/web_httpd_idle.c').read_text()
|
||||
assert 'ESP_IDF_VERSION_VAL(5, 5, 0)' in adapter
|
||||
for forbidden in ('httpd_sess_trigger_close', 'web_diagnostics', 'xTaskCreate', 'malloc(', 'calloc(', 'ESP_LOG'):
|
||||
assert forbidden not in idle, forbidden
|
||||
sweep = function(adapter, 'web_httpd_idle_sweep')
|
||||
assert 'httpd_sess_trigger_close' not in sweep
|
||||
assert 'web_httpd_idle_tls(arg);\n web_diagnostics_tls(arg);' in (ROOT / 'src/web_server.c').read_text()
|
||||
assert 'setInterval(pollStatus, 5000)' in (ROOT / 'src/web_ui.c').read_text()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='web-httpd-idle-') as directory:
|
||||
directory = Path(directory)
|
||||
unit = directory / 'test.c'
|
||||
unit.write_text((HERE / 'fakes.h').read_text() + '\n' +
|
||||
stripped(ROOT / 'src/web_httpd_adapter.h') + '\n' +
|
||||
stripped(ROOT / 'src/web_httpd_idle.h') + '\n' +
|
||||
sweep + '\n' + stripped(ROOT / 'src/web_httpd_idle.c') + '\n' +
|
||||
function(parse, 'httpd_req_delete') + '\n' + completion + '\n' +
|
||||
(HERE / 'test.c').read_text())
|
||||
executable = directory / 'test'
|
||||
subprocess.run([os.environ.get('CC', 'cc'), '-std=c11', '-Wall', '-Wextra', '-Werror',
|
||||
'-g', str(unit), '-o', str(executable)], check=True, timeout=30)
|
||||
subprocess.run([str(executable)], check=True, timeout=20)
|
||||
print('PASS installed SDK completion/owner-order and production integration guards')
|
||||
@@ -0,0 +1,329 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
static struct sock_db sockets[WEB_HTTPD_IDLE_SOCKETS];
|
||||
static struct httpd_data server;
|
||||
static int peers[WEB_HTTPD_IDLE_SOCKETS];
|
||||
static esp_tls_t tls_identity; /* Deliberately reuse this identity and descriptors. */
|
||||
static int64_t handler_delay, purge_delay;
|
||||
static size_t leftover;
|
||||
static bool request_fail, purge_fail, upgrade;
|
||||
static unsigned purges, responses;
|
||||
|
||||
static void pump(void)
|
||||
{
|
||||
assert(!lock_depth && !server.hd_req_aux.sd);
|
||||
if (!queued_work) return;
|
||||
void (*work)(void *) = queued_work;
|
||||
void *arg = queued_arg;
|
||||
queued_work = NULL; queued_arg = NULL;
|
||||
bool previous = owner; owner = true; work(arg); owner = previous;
|
||||
}
|
||||
|
||||
static esp_err_t httpd_queue_work(httpd_handle_t hd, void (*work)(void *), void *arg)
|
||||
{
|
||||
assert(!lock_depth && hd == &server && !queued_work);
|
||||
++queue_calls;
|
||||
if (queue_error != ESP_OK) return queue_error;
|
||||
queued_work = work; queued_arg = arg;
|
||||
if (inline_work) pump();
|
||||
if (queue_hook) queue_hook();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int pending(httpd_handle_t hd, int fd)
|
||||
{
|
||||
assert(owner && !lock_depth && hd == &server);
|
||||
return tls_pending[fd];
|
||||
}
|
||||
|
||||
static void tick(int64_t at)
|
||||
{
|
||||
now_us = at;
|
||||
bool previous = owner; owner = false; timer_callback(NULL); owner = previous;
|
||||
pump();
|
||||
}
|
||||
|
||||
static void drop(unsigned i)
|
||||
{
|
||||
if (sockets[i].fd >= 0) {
|
||||
owner = true;
|
||||
tls_identity.fd = sockets[i].fd;
|
||||
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CLOSE};
|
||||
web_httpd_idle_tls(&arg);
|
||||
close(sockets[i].fd); sockets[i].fd = -1;
|
||||
}
|
||||
if (peers[i] >= 0) { close(peers[i]); peers[i] = -1; }
|
||||
}
|
||||
|
||||
static void connect_slot(unsigned i, int reuse_fd)
|
||||
{
|
||||
int pair[2]; assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0);
|
||||
if (reuse_fd >= 0 && pair[0] != reuse_fd) {
|
||||
assert(pair[1] != reuse_fd && dup2(pair[0], reuse_fd) == reuse_fd);
|
||||
close(pair[0]); pair[0] = reuse_fd;
|
||||
}
|
||||
assert(pair[0] < FD_SETSIZE);
|
||||
sockets[i] = (struct sock_db){.fd = pair[0], .pending_fn = pending};
|
||||
peers[i] = pair[1]; tls_identity = (esp_tls_t){.fd = pair[0]};
|
||||
owner = true;
|
||||
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CREATE};
|
||||
web_httpd_idle_tls(&arg);
|
||||
}
|
||||
|
||||
static void reset(void)
|
||||
{
|
||||
/* Each test models a fresh process; restart tests below do NOT reset state. */
|
||||
memset(s_rows, 0, sizeof(s_rows));
|
||||
s_server = s_timer = NULL; s_generation = 0;
|
||||
s_accepting = s_queued = s_submitting = false;
|
||||
server = (struct httpd_data){.config.max_open_sockets = 6, .hd_td.handle = (void *)1, .hd_sd = sockets};
|
||||
memset(sockets, 0, sizeof(sockets));
|
||||
for (unsigned i = 0; i < 6; ++i) sockets[i].fd = peers[i] = -1;
|
||||
memset(tls_pending, 0, sizeof(tls_pending));
|
||||
owner = true; now_us = 0; lock_depth = shutdown_calls = select_calls = 0;
|
||||
shutdown_error = select_error = 0;
|
||||
timer_create_error = timer_start_error = queue_error = ESP_OK;
|
||||
timer_creates = timer_starts = timer_deletes = queue_calls = 0;
|
||||
timer_callback = queued_work = NULL; queued_arg = NULL; queue_hook = delay_hook = NULL;
|
||||
inline_work = request_fail = purge_fail = upgrade = false;
|
||||
handler_delay = purge_delay = 0; leftover = purges = responses = 0;
|
||||
}
|
||||
|
||||
static void start(void)
|
||||
{
|
||||
assert(web_httpd_idle_prepare() == ESP_OK);
|
||||
assert(web_httpd_idle_attach(&server) == ESP_OK);
|
||||
}
|
||||
|
||||
static void stop(void)
|
||||
{
|
||||
owner = false; assert(web_httpd_idle_detach(&server) == ESP_OK);
|
||||
/* Model successful HTTPD stop: join work, close sessions, destroy queue. */
|
||||
pump();
|
||||
for (unsigned i = 0; i < 6; ++i) drop(i);
|
||||
queued_work = NULL; queued_arg = NULL;
|
||||
web_httpd_idle_stopped(&server);
|
||||
assert(!s_server && !s_queued && !s_submitting);
|
||||
}
|
||||
|
||||
static esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
|
||||
{
|
||||
assert(owner && hd == &server && !hd->hd_req_aux.sd);
|
||||
hd->hd_req_aux.sd = sd;
|
||||
hd->hd_req_aux.remaining_len = leftover;
|
||||
hd->hd_req = (httpd_req_t){.handle = hd, .aux = &hd->hd_req_aux};
|
||||
/* Parser/handler/send run synchronously; timers can queue but cannot run
|
||||
* work on HTTPD until this and the SDK's leftover-body purge return. */
|
||||
timer_callback(NULL);
|
||||
now_us += handler_delay;
|
||||
unsigned before = shutdown_calls;
|
||||
web_httpd_idle_sweep(hd, s_rows, now_us); /* defensive active-owner guard */
|
||||
assert(shutdown_calls == before);
|
||||
if (request_fail) { httpd_req_cleanup(&hd->hd_req); return ESP_FAIL; }
|
||||
++responses;
|
||||
if (upgrade) sd->ws_handshake_done = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int httpd_req_recv(httpd_req_t *req, char *data, size_t size)
|
||||
{
|
||||
assert(owner && req->aux && req->aux->sd && size);
|
||||
++purges; now_us += purge_delay;
|
||||
timer_callback(NULL);
|
||||
if (purge_fail) return -1;
|
||||
memset(data, 0, size); req->aux->remaining_len -= size;
|
||||
return (int)size;
|
||||
}
|
||||
|
||||
static void httpd_req_cleanup(httpd_req_t *req)
|
||||
{
|
||||
req->aux->sd = NULL; req->aux = NULL; req->handle = NULL;
|
||||
}
|
||||
|
||||
static void complete(unsigned i)
|
||||
{
|
||||
owner = true;
|
||||
if (httpd_sess_process(&server, &sockets[i]) != ESP_OK) drop(i);
|
||||
pump();
|
||||
}
|
||||
|
||||
static void inline_submit_hook(void)
|
||||
{
|
||||
unsigned before = queue_calls;
|
||||
assert(!s_queued && s_submitting);
|
||||
idle_timer(NULL); assert(queue_calls == before);
|
||||
}
|
||||
|
||||
static void stop_during_submit(void)
|
||||
{
|
||||
assert(s_submitting && s_queued);
|
||||
assert(web_httpd_idle_detach(&server) == ESP_ERR_TIMEOUT);
|
||||
assert(s_server == &server && !s_accepting && s_submitting);
|
||||
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
reset(); timer_create_error = ESP_ERR_NO_MEM;
|
||||
assert(web_httpd_idle_prepare() == ESP_ERR_NO_MEM && !s_timer && !s_server);
|
||||
timer_create_error = ESP_OK; timer_start_error = ESP_FAIL;
|
||||
assert(web_httpd_idle_prepare() == ESP_FAIL && !s_timer && timer_deletes == 1);
|
||||
timer_start_error = ESP_OK; start();
|
||||
assert(timer_creates == 3 && timer_starts == 2);
|
||||
stop(); start(); assert(timer_creates == 3); stop();
|
||||
puts("PASS timer failure cleanup/retry and one persistent timer across restart");
|
||||
|
||||
reset(); start();
|
||||
for (unsigned i = 0; i < 6; ++i) connect_slot(i, -1);
|
||||
sockets[4].ws_handshake_done = sockets[5].ws_handshake_done = true;
|
||||
tick(0); tick(14999999); assert(!shutdown_calls);
|
||||
tick(15000000); assert(shutdown_calls == 4);
|
||||
tick(16000000); assert(shutdown_calls == 4);
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
char byte; assert(recv(peers[i], &byte, 1, MSG_DONTWAIT) == 0);
|
||||
/* HTTPD's next read owns TLS/free/slot retirement, not the sweep. */
|
||||
assert(sockets[i].fd >= 0); drop(i);
|
||||
}
|
||||
connect_slot(0, -1); sockets[0].ws_handshake_done = true; /* newly admitted admin */
|
||||
tick(60000000); assert(shutdown_calls == 4); stop();
|
||||
puts("PASS six full slots: only four expired ordinary sockets shut down; two serial and new admin WS survive");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
for (unsigned i = 1; i <= 12; ++i) { now_us = (int64_t)i * 5000000; complete(0); }
|
||||
assert(responses == 12 && !shutdown_calls && s_rows[0].idle_since_us == now_us);
|
||||
tick(now_us + 15000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS actual SDK completion marker refreshes five-second ordinary polling without TLS churn");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
assert(send(peers[0], "G", 1, 0) == 1); tick(15000000); assert(!shutdown_calls);
|
||||
char byte; assert(recv(sockets[0].fd, &byte, 1, 0) == 1);
|
||||
now_us = 16000000; complete(0); tick(30000000); assert(!shutdown_calls);
|
||||
tick(31000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS control-before-data ordering: readable incomplete next request is not expired");
|
||||
|
||||
for (unsigned mode = 0; mode < 4; ++mode) {
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
if (mode == 0) sockets[0].pending_len = 1;
|
||||
if (mode == 1) tls_pending[sockets[0].fd] = 1;
|
||||
if (mode == 2) tls_pending[sockets[0].fd] = -1;
|
||||
if (mode == 3) select_error = 1;
|
||||
tick(15000000); tick(60000000); assert(!shutdown_calls);
|
||||
sockets[0].pending_len = 0; tls_pending[sockets[0].fd] = select_error = 0;
|
||||
tick(74999999); assert(!shutdown_calls); tick(75000000); assert(shutdown_calls == 1); stop();
|
||||
}
|
||||
puts("PASS HTTPD pipeline/TLS buffered input and TLS/select errors conservatively restart idle window");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
handler_delay = 40000000; purge_delay = 10000000; leftover = 64;
|
||||
now_us = 14000000; complete(0);
|
||||
assert(now_us == 74000000 && purges == 2 && responses == 1 && !shutdown_calls);
|
||||
assert(s_rows[0].completed == 1 && s_rows[0].idle_since_us == now_us);
|
||||
tick(88999999); assert(!shutdown_calls); tick(89000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS slow parser/handler/response plus real SDK leftover purge: queued probe observes completion only afterwards");
|
||||
|
||||
for (unsigned mode = 0; mode < 2; ++mode) {
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
request_fail = mode == 0; purge_fail = mode == 1; leftover = 64;
|
||||
handler_delay = 60000000; complete(0);
|
||||
assert(sockets[0].fd == -1 && server.lru_counter == 0 && !shutdown_calls); stop();
|
||||
}
|
||||
puts("PASS request/purge failure deletion precedes work and never publishes successful completion");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0); now_us = 14000000;
|
||||
upgrade = true; complete(0); tick(60000000);
|
||||
assert(sockets[0].ws_handshake_done && !s_rows[0].observed && !shutdown_calls); stop();
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
sockets[0].for_async_req = true; tick(60000000); assert(!shutdown_calls && !s_rows[0].observed);
|
||||
sockets[0].for_async_req = false; tick(61000000); tick(75999999); assert(!shutdown_calls);
|
||||
tick(76000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS admission upgrade classification before idle publication and async response exemption");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
int fd = sockets[0].fd; drop(0); now_us = 14000000; connect_slot(0, fd);
|
||||
/* Both old and new lru=0, same fd, TLS pointer and sock_db address. */
|
||||
tick(15000000); assert(!shutdown_calls && s_rows[0].idle_since_us == 15000000);
|
||||
tick(30000000); assert(shutdown_calls == 1);
|
||||
drop(0); connect_slot(0, fd); sockets[0].ws_handshake_done = true;
|
||||
tick(90000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS identical fd/TLS/sock_db/LRU reuse resets identity; no late shutdown targets replacement WS");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
shutdown_error = 1; tick(15000000); assert(shutdown_calls == 1 && !s_rows[0].shutdown_sent);
|
||||
shutdown_error = 0; tick(16000000); assert(shutdown_calls == 2 && s_rows[0].shutdown_sent);
|
||||
tick(17000000); assert(shutdown_calls == 2); stop();
|
||||
puts("PASS failed shutdown retries, successful shutdown is latched until normal owner deletion");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
queue_error = ESP_FAIL; tick(15000000); tick(16000000);
|
||||
assert(!s_queued && !s_submitting && !shutdown_calls);
|
||||
queue_error = ESP_OK; tick(17000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS queue failures release reservations and later probe enforces unchanged deadline");
|
||||
|
||||
reset(); start(); connect_slot(0, -1);
|
||||
idle_timer(NULL); void *old_arg = queued_arg;
|
||||
for (unsigned i = 0; i < 100; ++i) idle_timer(NULL);
|
||||
assert(queue_calls == 1 && s_queued); pump();
|
||||
inline_work = true; queue_hook = inline_submit_hook;
|
||||
idle_timer(NULL); assert(queue_calls == 2 && !s_submitting && !s_queued);
|
||||
queue_hook = NULL; inline_work = false; stop();
|
||||
puts("PASS at most one probe; callback-before-submit-return cannot clear or submit a newer probe");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
now_us = 20000000; queue_hook = stop_during_submit;
|
||||
idle_timer(NULL); queue_hook = NULL;
|
||||
assert(!s_submitting && s_queued && !s_accepting && !shutdown_calls);
|
||||
assert(web_httpd_idle_detach(&server) == ESP_OK);
|
||||
/* Failed SSL stop: MUST NOT call stopped, prepare/restart remains blocked. */
|
||||
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_httpd_idle_attach(&server) == ESP_ERR_INVALID_STATE);
|
||||
pump(); assert(!s_queued && !shutdown_calls && s_server == &server);
|
||||
unsigned calls = queue_calls; tick(60000000); assert(queue_calls == calls);
|
||||
stop(); start(); assert(s_accepting); stop();
|
||||
puts("PASS in-flight submit fence timeout forbids stop; failed stop disables probes but retains ownership until retry");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0); idle_timer(NULL); old_arg = queued_arg;
|
||||
assert(web_httpd_idle_detach(&server) == ESP_OK && s_queued);
|
||||
/* Successful SDK stop may discard, rather than run, the queued work. */
|
||||
queued_work = NULL; queued_arg = NULL; drop(0); web_httpd_idle_stopped(&server);
|
||||
assert(!s_queued); start(); connect_slot(0, -1); idle_timer(NULL);
|
||||
owner = true; idle_work(old_arg);
|
||||
assert(s_queued && queued_work && !shutdown_calls); pump(); stop();
|
||||
puts("PASS successful stop retires discarded queue; stale generation cannot act on reused server or clear new probe");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0);
|
||||
tls_identity.get_error = true;
|
||||
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CREATE};
|
||||
web_httpd_idle_tls(&arg); assert(!s_rows[0].observed);
|
||||
tick(15000000); assert(!shutdown_calls); stop();
|
||||
puts("PASS unavailable TLS identity fails conservatively, independent of diagnostics");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0); now_us = 60000000;
|
||||
owner = false; web_httpd_idle_sweep(&server, s_rows, now_us); assert(!shutdown_calls);
|
||||
owner = true; server.config.max_open_sockets = 7;
|
||||
web_httpd_idle_sweep(&server, s_rows, now_us); assert(!shutdown_calls);
|
||||
server.config.max_open_sockets = 6; sockets[0].ws_close = true;
|
||||
tick(now_us); assert(!shutdown_calls); stop();
|
||||
s_generation = UINTPTR_MAX;
|
||||
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_httpd_idle_attach(&server) == ESP_ERR_INVALID_STATE);
|
||||
puts("PASS owner/capacity/closing-WS guards and nonwrapping restart generation");
|
||||
reset(); start(); idle_timer(NULL);
|
||||
/* TLS establishment occupies HTTPD synchronously; only timer task runs. */
|
||||
for (unsigned i = 0; i < 5; ++i) { now_us += 1000000; idle_timer(NULL); }
|
||||
assert(queue_calls == 1 && !shutdown_calls && !s_rows[0].observed);
|
||||
connect_slot(0, -1); pump();
|
||||
assert(s_rows[0].idle_since_us == 5000000);
|
||||
tick(19999999); assert(!shutdown_calls); tick(20000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS serialized TLS handshake delays probe; post-TLS unused connection gets a fresh full idle window");
|
||||
|
||||
reset(); start(); connect_slot(0, -1); tick(0); idle_timer(NULL);
|
||||
/* Nonblocking IDF control transport is UDP: success is not an execution
|
||||
* acknowledgement. Never time out the reservation and accumulate probes. */
|
||||
queued_work = NULL; queued_arg = NULL; calls = queue_calls;
|
||||
tick(60000000); tick(120000000);
|
||||
assert(s_queued && queue_calls == calls && !shutdown_calls);
|
||||
stop(); start(); connect_slot(0, -1); tick(120000000);
|
||||
tick(135000000); assert(shutdown_calls == 1); stop();
|
||||
puts("PASS accepted-but-lost UDP work remains bounded/fail-safe; successful stop/restart restores probing");
|
||||
puts("18 idle lifecycle groups passed (real socket IO, installed SDK completion/purge, deterministic TLS/scheduler)");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user