Add Bounded Ordinary HTTPS Idle Cleanup

This commit is contained in:
2026-09-08 18:33:33 +02:00
parent f6263042ff
commit 82f21d6116
18 changed files with 884 additions and 13 deletions
+17
View File
@@ -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).
+84
View File
@@ -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 *);
+62
View File
@@ -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')
+329
View File
@@ -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;
}