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