Send binary WebSocket frames in one write

This commit is contained in:
2026-09-08 23:52:00 +02:00
parent 042499e4d6
commit 60d9c54bb4
10 changed files with 334 additions and 8 deletions
+33
View File
@@ -2,6 +2,7 @@
"""Compile actual transport callbacks using the existing serial/store doubles."""
import os
import pathlib
import re
import subprocess
import sys
import tempfile
@@ -13,9 +14,32 @@ sys.path.insert(0, str(BASE))
from run import HEADERS
from serial_headers import SERIAL_HEADERS
os.environ['CCACHE_DISABLE'] = '1'
IDF = pathlib.Path(os.environ.get('IDF_PATH', str(pathlib.Path.home() / '.platformio/packages/framework-espidf')))
def function(source, name):
match = re.search(r'^(?:static )?(?:esp_err_t|int|ssize_t) ' + name + r'\(.*?^\}', source, re.M | re.S)
assert match, name
return match.group() + '\n'
adapter = (ROOT / 'src/web_httpd_adapter.c').read_text()
assert 'ESP_IDF_VERSION_VAL(5, 5, 0)' in adapter
ws_source = (IDF / 'components/esp_http_server/src/httpd_ws.c').read_text()
ws = function(ws_source, 'httpd_ws_send_frame_async')
main = (IDF / 'components/esp_http_server/src/httpd_main.c').read_text()
assert main.index('/* Case0:') < main.index('httpd_process_ctrl_msg(hd);') < main.index('/* Case1:')
parse = (IDF / 'components/esp_http_server/src/httpd_parse.c').read_text()
assert parse.index('if (sd->ws_close == true)') < parse.index('ret = httpd_ws_get_frame_type(r);')
ssl_msg = (IDF / 'components/mbedtls/mbedtls/library/ssl_msg.c').read_text()
assert 'to be called with the same parameters' in ssl_msg
assert ws.count('sess->send_fn(') == 2
https = function((IDF / 'components/esp_https_server/src/https_server.c').read_text(), 'httpd_ssl_send')
assert 'esp_tls_conn_write(tls, buf, buf_len)' in https and 'return ret;' in https
tls = function((IDF / 'components/esp-tls/esp_tls_mbedtls.c').read_text(), 'esp_mbedtls_write')
assert 'return (written > 0) ? written : ret;' in tls and 'return written;' in tls
with tempfile.TemporaryDirectory(prefix='web-performance-') as directory:
tmp = pathlib.Path(directory)
headers = HEADERS | SERIAL_HEADERS
headers['esp_http_server.h'] += '\n#define HTTPD_SOCK_ERR_FAIL -1\n'
headers['freertos/FreeRTOS.h'] = headers['freertos/FreeRTOS.h'].replace(
'extern int host_lock_depth;', 'extern int host_lock_depth; void host_before_lock(void);').replace(
'(void)(m); assert(host_lock_depth++', '(void)(m); host_before_lock(); assert(host_lock_depth++')
@@ -27,6 +51,14 @@ with tempfile.TemporaryDirectory(prefix='web-performance-') as directory:
fixture = fixture.replace('#include "test.c"', '#include "' + str(BASE / 'test.c') + '"')
fixture = fixture.replace('../../src/web_serial_transport.c', str(ROOT / 'src/web_serial_transport.c'))
(tmp / 'fixture.c').write_text(fixture)
(tmp / 'binary.inc').write_text(function(adapter, 'web_httpd_aborted_send') + function(adapter, 'web_httpd_ws_send_binary'))
(tmp / 'sdk_ws.inc').write_text(ws.replace('httpd_ws_send_frame_async(', 'sdk_ws_send_frame('))
automatic = ''.join(function(ws_source, name) for name in
('httpd_ws_check_req', 'httpd_ws_send_frame', 'httpd_ws_get_frame_type'))
automatic = automatic.replace('httpd_ws_send_frame_async(', 'sdk_ws_send_frame(')
automatic = automatic.replace('httpd_ws_send_frame(', 'sdk_ws_send_request(')
automatic = automatic.replace('httpd_ws_recv_frame(', 'sdk_control_recv(')
(tmp / 'sdk_automatic.inc').write_text(automatic)
console = (ROOT / 'src/web_console.c').read_text()
start = console.index('static void print_performance_time(')
end = console.index('static int command_web(', start)
@@ -39,3 +71,4 @@ with tempfile.TemporaryDirectory(prefix='web-performance-') as directory:
'-Wl,--gc-sections', str(HERE / 'test.c'), str(ROOT / 'src/web_session_store.c'),
str(ROOT / 'src/web_auth_parse.c'), '-o', str(tmp / 'test')], check=True, timeout=30)
subprocess.run([str(tmp / 'test')], check=True, timeout=15)
print('PASS installed IDF WS two-write / HTTPS forwarding / TLS partial-return contract guards')
+212 -5
View File
@@ -6,7 +6,83 @@ static void *queued_arg;
static bool early;
static esp_err_t queue_result, send_result;
static size_t read_bytes;
static unsigned sends;
static unsigned sends, control_sends, lru_updates;
/* Only private HTTPD layout/owner and the low-level TLS send are doubled. */
struct sock_db {
int fd;
void *ctx;
bool ws_handshake_done, ws_close, for_async_req, ws_control_frames;
int (*send_fn)(httpd_handle_t, int, const char *, size_t, int);
};
struct httpd_req_aux {
struct sock_db *sd;
bool ws_final;
httpd_ws_type_t ws_type;
};
struct httpd_data {
struct { unsigned handle; } hd_td;
struct { struct sock_db *sd; } hd_req_aux;
};
static struct httpd_data host_hd;
static struct sock_db host_sd;
static unsigned owner;
static int raw_result;
static bool override_result;
static uint8_t captured[516];
static size_t captured_length;
static unsigned httpd_os_thread_handle(void) { return owner; }
static struct sock_db *httpd_sess_get(httpd_handle_t h, int fd)
{ assert(h == &host_hd); return fd == host_sd.fd ? &host_sd : NULL; }
static unsigned shutdown_calls, shutdown_delay;
static int shutdown_result;
#define SHUT_RDWR 2
static int host_shutdown(int fd, int how)
{
assert(!host_lock_depth && owner == host_hd.hd_td.handle);
assert(fd == host_sd.fd && how == SHUT_RDWR && host_sd.ws_close);
/* The send barrier must already hold even when shutdown fails. */
assert(host_sd.send_fn(&host_hd, fd, "ignored", 7, 0) == HTTPD_SOCK_ERR_FAIL);
++shutdown_calls; now += shutdown_delay;
return shutdown_result;
}
#define shutdown host_shutdown
#include "binary.inc"
#undef shutdown
#define HTTPD_WS_FIN_BIT 0x80
#define HTTPD_WS_CONTINUE 0
#define HTTPD_WS_MASK_BIT 0x80
#ifndef ESP_LOGW
#define ESP_LOGW(...) ((void)0)
#endif
#include "sdk_ws.inc"
#define HTTPD_WS_OPCODE_BITS 0x0f
#define HTTPD_WS_TYPE_CLOSE 8
#define HTTPD_WS_TYPE_PING 9
#define HTTPD_WS_TYPE_PONG 10
static uint8_t incoming_opcode;
static unsigned automatic_reads;
static int httpd_recv_with_opt(httpd_req_t *r, char *out, size_t n, bool peek)
{
(void)r; assert(n == 1 && !peek); *out = 0x80 | incoming_opcode;
++automatic_reads; return 1;
}
static esp_err_t sdk_control_recv(httpd_req_t *r, httpd_ws_frame_t *f, size_t n)
{
(void)r; assert(n == 126); f->len = 1; f->payload[0] = 0x5a;
++automatic_reads; return ESP_OK;
}
#define ESP_LOGD(...) ((void)0)
#include "sdk_automatic.inc"
static int tls_send(httpd_handle_t h, int fd, const char *data, size_t n, int flags)
{
assert(!host_lock_depth && h == &host_hd && fd == host_sd.fd && flags == 0);
assert(owner == host_hd.hd_td.handle && captured_length + n <= sizeof(captured));
memcpy(captured + captured_length, data, n); captured_length += n;
++sends; now += 11;
if (send_hook) send_hook();
return override_result ? raw_result : send_result == ESP_OK ? (int)n : -1;
}
static unsigned lock_delay;
static bool disconnect_stalled;
void host_before_lock(void) { now += lock_delay; lock_delay = 0; }
@@ -25,18 +101,19 @@ esp_err_t httpd_queue_work(httpd_handle_t h, void (*fn)(void *), void *arg)
(void)h; assert(!host_lock_depth);
queued = fn; queued_arg = arg;
if (queue_hook) queue_hook();
if (early && queue_result == ESP_OK) { now += 7; fn(arg); }
if (early && queue_result == ESP_OK) { captured_length = 0; now += 7; fn(arg); }
return queue_result;
}
esp_err_t httpd_ws_send_frame_async(httpd_handle_t h, int fd, httpd_ws_frame_t *f)
{
(void)h; (void)fd; assert(!host_lock_depth); assert(f->payload == s_slots[0].tx_data);
++sends; now += 11;
assert(f->type == HTTPD_WS_TYPE_TEXT);
++control_sends; now += 11;
if (send_hook) send_hook();
return send_result;
}
esp_err_t httpd_sess_update_lru_counter(httpd_handle_t h, int fd)
{ (void)h; (void)fd; assert(!host_lock_depth); now += 3; return ESP_OK; }
{ (void)h; (void)fd; assert(!host_lock_depth); ++lru_updates; now += 3; return ESP_OK; }
esp_err_t session_broker_read(session_broker_client_id_t id, uint8_t *data, size_t n, size_t *out)
{
(void)id; (void)data; assert(!host_lock_depth && read_bytes <= n);
@@ -45,7 +122,13 @@ esp_err_t session_broker_read(session_broker_client_id_t id, uint8_t *data, size
}
static web_serial_slot_t *setup(void)
{
request.handle = &host_hd;
serial_reset(); issued_t a = mint(&alice); web_serial_slot_t *s = connect_session(&a);
host_hd = (struct httpd_data){.hd_td.handle = 1}; owner = 1;
host_sd = (struct sock_db){.fd = 10, .ctx = s, .ws_handshake_done = true, .send_fn = tls_send};
override_result = false; captured_length = 0; control_sends = 0; lru_updates = 0;
shutdown_calls = shutdown_delay = automatic_reads = 0; shutdown_result = 0;
memset(&s_counters, 0, sizeof(s_counters));
early = false; disconnect_stalled = false; lock_delay = 0; queue_result = send_result = ESP_OK;
send_hook = queue_hook = read_hook = NULL; read_bytes = 0; sends = 0;
assert(web_serial_performance_enable(false) == ESP_OK);
@@ -56,7 +139,7 @@ static void enable(void) { assert(web_serial_performance_enable(true) == ESP_OK)
static void clear(void) { assert(web_serial_performance_clear() == ESP_OK); }
static void disable(void) { assert(web_serial_performance_enable(false) == ESP_OK); }
static void cycle(void) { disable(); enable(); }
static void run_work(void) { assert(queued); queued(queued_arg); }
static void run_work(void) { assert(queued); captured_length = 0; queued(queued_arg); }
static void queue_binary(web_serial_slot_t *s)
{ assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 512) == ESP_OK); }
static web_serial_performance_session_t row(void)
@@ -65,9 +148,133 @@ static web_serial_performance_session_t row(void)
assert(web_serial_performance_snapshot(&s) == ESP_OK);
return s.sessions[0];
}
static void binary_tests(void)
{
/* Compare every supported length and arbitrary bytes with installed IDF's
* actual serializer, but require one override call instead of two. */
for (size_t length = 0; length <= 512; ++length) {
web_serial_slot_t *s = setup();
for (size_t i = 0; i < sizeof(s->tx_data); ++i) s->tx_data[i] = (uint8_t)i;
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_BINARY,
.payload = s->tx_data, .len = length};
assert(sdk_ws_send_frame(request.handle, 10, &frame) == ESP_OK);
uint8_t expected[516]; size_t expected_length = captured_length;
memcpy(expected, captured, expected_length);
assert(sends == (length ? 2U : 1U));
captured_length = 0; sends = 0;
assert(web_httpd_ws_send_binary(request.handle, 10, s, s->tx_data, length) == ESP_OK);
assert(sends == 1 && captured_length == expected_length);
assert(!memcmp(captured, expected, expected_length));
for (size_t i = 0; i < sizeof(s->tx_data); ++i) assert(s->tx_data[i] == (uint8_t)i);
}
/* Every short result, including an entirely consumed header, fails closed.
* No second call/replay, no successful byte/frame or LRU accounting. */
for (int result = -4; result <= 517; ++result) {
if (result == 516) continue;
web_serial_slot_t *s = setup(); enable(); override_result = true; raw_result = result;
queue_binary(s); run_work();
assert(sends == 1 && !control_sends && s->close_requested && !s->work_pending);
assert(row().send_errors == 1 && row().send_call.sum_us == 11 && !row().sent_frames);
assert(s_counters.send_failures == 1 && !s_counters.tx_binary_bytes && !lru_updates);
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 1) == ESP_ERR_INVALID_STATE);
run_work(); assert(sends == 1);
}
const int tls_errors[] = {-0x6880, -0x6900, -0x0050, -0x004e};
for (size_t i = 0; i < sizeof(tls_errors)/sizeof(tls_errors[0]); ++i) {
web_serial_slot_t *s = setup(); enable(); override_result = true; raw_result = tls_errors[i];
queue_binary(s); run_work(); assert(sends == 1 && s->close_requested && row().send_errors == 1);
}
for (unsigned invalid = 0; invalid < 7; ++invalid) {
web_serial_slot_t *s = setup(); enable(); queue_binary(s);
switch (invalid) {
case 0: owner = 2; break;
case 1: host_hd.hd_req_aux.sd = &host_sd; break;
case 2: host_sd.fd = 11; break;
case 3: host_sd.ctx = &s_slots[1]; break;
case 4: host_sd.ws_handshake_done = false; break;
case 5: host_sd.ws_close = true; break;
case 6: host_sd.for_async_req = true; break;
}
run_work(); assert(!sends && s->close_requested && row().send_errors == 1);
assert(!shutdown_calls && host_sd.send_fn == tls_send);
}
web_serial_slot_t *s = setup();
host_sd.send_fn = NULL;
assert(web_httpd_ws_send_binary(request.handle, 10, s, s->tx_data, 1) == ESP_ERR_INVALID_STATE);
host_sd.send_fn = tls_send;
assert(web_httpd_ws_send_binary(NULL, 10, s, s->tx_data, 1) == ESP_ERR_INVALID_ARG);
assert(web_httpd_ws_send_binary(request.handle, -1, s, s->tx_data, 1) == ESP_ERR_INVALID_ARG);
assert(web_httpd_ws_send_binary(request.handle, 10, NULL, s->tx_data, 1) == ESP_ERR_INVALID_ARG);
assert(web_httpd_ws_send_binary(request.handle, 10, s, NULL, 1) == ESP_ERR_INVALID_ARG);
assert(web_httpd_ws_send_binary(request.handle, 10, s, s->tx_data, 513) == ESP_ERR_INVALID_ARG);
assert(!sends);
s = setup(); enable(); queue_binary(s);
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 512) == ESP_ERR_INVALID_STATE);
run_work(); assert(sends == 1 && row().sent_bytes == 512 && lru_updates == 1);
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_TEXT, 4) == ESP_OK); run_work();
assert(control_sends == 1 && sends == 1 && row().sent_frames == 1);
/* Both fixed slots can retain one independent payload. HTTPD still sends
* serially; failure of the first must not retire the second reservation. */
s = setup(); issued_t other = mint(&bob); web_serial_slot_t *second = connect_session(&other);
memset(s->tx_data, 0xa5, sizeof(s->tx_data));
memset(second->tx_data, 0x5a, sizeof(second->tx_data));
queue_binary(s); queue_binary(second);
request.sess_ctx = s; host_sd.ctx = s; override_result = true; raw_result = 2;
web_serial_send_work(&s->work);
assert(s->close_requested && second->work_pending && !second->close_requested);
captured_length = 0; request.sess_ctx = second; override_result = false;
host_sd = (struct sock_db){.fd = 10, .ctx = second, .ws_handshake_done = true, .send_fn = tls_send};
web_serial_send_work(&second->work);
assert(sends == 2 && !second->work_pending && !second->close_requested);
for (size_t i = 4; i < captured_length; ++i) assert(captured[i] == 0x5a);
assert(s_counters.tx_binary_bytes == 512 && s_counters.send_failures == 1);
printf("PASS binary single-write: 513 SDK wire comparisons, short/zero/TLS errors, owner/session gates, two-slot isolation, one outstanding, text isolation\n");
}
static void abort_tests(void)
{
const int results[] = {-0x6880, -0x6900, -0x0050, 0, 2, 515};
for (unsigned i = 0; i < sizeof(results)/sizeof(results[0]); ++i) {
for (int failure = 0; failure < 2; ++failure) {
web_serial_slot_t *s = setup(); enable();
override_result = true; raw_result = results[i];
shutdown_result = failure ? -1 : 0; shutdown_delay = 5;
unsigned closes_before = closes;
queue_binary(s); run_work();
assert(sends == 1 && shutdown_calls == 1 && closes == closes_before);
assert(s->close_requested && !s->work_pending && host_sd.ws_close);
assert(row().send_call.sum_us == 16 && row().send_errors == 1 && !lru_updates);
/* Deliberately invoke actual SDK automatic control processing even
* though ws_close would normally skip it in httpd_req_new. Model
* buffered input after shutdown, including failed shutdown. No
* deferred close or transport-owner iteration has run yet. */
struct httpd_req_aux aux = {.sd = &host_sd};
httpd_req_t req = {.handle = &host_hd, .aux = &aux};
const uint8_t opcodes[] = {HTTPD_WS_TYPE_PING, HTTPD_WS_TYPE_CLOSE};
override_result = false; captured_length = 0;
for (unsigned j = 0; j < sizeof(opcodes); ++j) {
incoming_opcode = opcodes[j];
assert(httpd_ws_get_frame_type(&req) == ESP_FAIL);
assert(sends == 1 && !captured_length && closes == closes_before);
assert(aux.ws_type == incoming_opcode && aux.ws_final);
}
assert(automatic_reads == 4 && shutdown_calls == 1);
/* Ordinary HTTPD teardown/new-session initialization resets the
* per-session override: no global fd poison survives reuse. */
host_sd = (struct sock_db){.fd = 10, .ctx = &s_slots[1],
.ws_handshake_done = true, .send_fn = tls_send};
incoming_opcode = HTTPD_WS_TYPE_PING;
assert(httpd_ws_get_frame_type(&req) == ESP_OK && sends == 3);
}
}
puts("PASS owner abort before return: WANT/error/zero/short, shutdown success/failure, actual SDK automatic PING/CLOSE blocked before deferred cleanup, fd reuse");
}
int main(void)
{
assert(serial_tests() == 0);
binary_tests();
abort_tests();
web_serial_slot_t *s = setup();
queue_binary(s); run_work(); assert(row().sent_frames == 0); /* default off */
enable(); early = true; queue_binary(s);