From b39dfc970e77a4e417509cdfeb860e17e32e42c4 Mon Sep 17 00:00:00 2001 From: surengab Date: Wed, 25 Mar 2026 15:34:10 +0400 Subject: [PATCH] refactor(tcp_transport): move connection-closed socket polling from WS to base transport layer --- .../private_include/esp_transport_internal.h | 23 +++++++++- components/tcp_transport/transport_ssl.c | 42 ++++++++++++++++- components/tcp_transport/transport_ws.c | 46 +++---------------- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/components/tcp_transport/private_include/esp_transport_internal.h b/components/tcp_transport/private_include/esp_transport_internal.h index 6556ad3e45..c177de85cb 100644 --- a/components/tcp_transport/private_include/esp_transport_internal.h +++ b/components/tcp_transport/private_include/esp_transport_internal.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -136,6 +136,27 @@ void esp_transport_capture_errno(esp_transport_handle_t t, int sock_errno); */ void esp_transport_set_errors(esp_transport_handle_t t, const esp_tls_error_handle_t error_handle); +/** + * @brief Polls the underlying socket until the connection is closed or the timeout expires. + * + * This is an internal helper used by higher-level transports (e.g. WebSocket) that need to wait + * for a clean TCP connection teardown after completing their own application-level close handshake. + * It selects on the read and error sets of the socket and distinguishes between: + * - Clean closure by FIN (recv returns 0 on a readable socket) + * - RST-based closure (ENOTCONN / ECONNRESET / ECONNABORTED on the error set) + * - Unexpected conditions (socket readable with actual data, or unrecognised errno) + * + * @param[in] t A base transport handle (TCP or SSL) whose socket is to be monitored. + * Must NOT be a WS transport handle; pass ws->parent from the WS layer. + * @param[in] timeout_ms Timeout in milliseconds (-1 to wait forever). + * + * @return + * - 1 Connection terminated: either clean FIN or expected RST errno + * - 0 Timeout: no activity within timeout_ms + * - -1 Error: socket unexpectedly readable with data, unrecognised errno, or invalid handle + */ +int esp_transport_poll_connection_closed(esp_transport_handle_t t, int timeout_ms); + #ifdef __cplusplus } #endif diff --git a/components/tcp_transport/transport_ssl.c b/components/tcp_transport/transport_ssl.c index 4a97b18ec4..104ed0962b 100644 --- a/components/tcp_transport/transport_ssl.c +++ b/components/tcp_transport/transport_ssl.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -519,6 +519,46 @@ static int base_get_socket(esp_transport_handle_t t) return INVALID_SOCKET; } +int esp_transport_poll_connection_closed(esp_transport_handle_t t, int timeout_ms) +{ + int sock = base_get_socket(t); + if (sock < 0) { + return -1; + } + + struct timeval timeout; + fd_set readset; + fd_set errset; + FD_ZERO(&readset); + FD_ZERO(&errset); + FD_SET(sock, &readset); + FD_SET(sock, &errset); + + int ret = select(sock + 1, &readset, NULL, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); + if (ret > 0) { + if (FD_ISSET(sock, &readset)) { + uint8_t buffer; + if (recv(sock, &buffer, 1, MSG_PEEK) <= 0) { + // Socket readable but zero bytes available: clean closure by FIN + return 1; + } + ESP_LOGW(TAG, "poll_connection_closed: unexpected data readable on socket=%d", sock); + } else if (FD_ISSET(sock, &errset)) { + int sock_errno = 0; + uint32_t optlen = sizeof(sock_errno); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); + ESP_LOGD(TAG, "poll_connection_closed select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), sock); + if (sock_errno == ENOTCONN || sock_errno == ECONNRESET || sock_errno == ECONNABORTED) { + // RST-based closure: treat as expected connection termination + return 1; + } + ESP_LOGE(TAG, "poll_connection_closed: unexpected errno=%d on socket=%d", sock_errno, sock); + } + return -1; + } + return ret; // 0 on timeout, -1 on select error +} + #ifdef CONFIG_ESP_TLS_USE_DS_PERIPHERAL void esp_transport_ssl_set_ds_data(esp_transport_handle_t t, void *ds_data) { diff --git a/components/tcp_transport/transport_ws.c b/components/tcp_transport/transport_ws.c index 328737d393..53802b5cfd 100644 --- a/components/tcp_transport/transport_ws.c +++ b/components/tcp_transport/transport_ws.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "esp_log.h" #include "esp_transport.h" @@ -18,7 +17,6 @@ #include "esp_transport_internal.h" #include "errno.h" #include "esp_tls_crypto.h" -#include static const char *TAG = "transport_ws"; @@ -815,9 +813,9 @@ void esp_transport_ws_set_path(esp_transport_handle_t t, const char *path) static int ws_get_socket(esp_transport_handle_t t) { if (t) { - transport_ws_t *ws = t->data; - if (ws && ws->parent && ws->parent->_get_socket) { - return ws->parent->_get_socket(ws->parent); + transport_ws_t *ws = esp_transport_get_context_data(t); + if (ws) { + return esp_transport_get_socket(ws->parent); } } return -1; @@ -1121,7 +1119,7 @@ static int esp_transport_ws_handle_control_frames(esp_transport_handle_t t, char // control frame handled correctly, reset the flag indicating new header received ws->frame_state.header_received = false; - int ret = esp_transport_ws_poll_connection_closed(t, timeout_ms); + int ret = esp_transport_poll_connection_closed(ws->parent, timeout_ms); if (ret == 0) { ESP_LOGW(TAG, "Connection cannot be terminated gracefully within timeout=%d", timeout_ms); return -1; @@ -1144,38 +1142,6 @@ static int esp_transport_ws_handle_control_frames(esp_transport_handle_t t, char int esp_transport_ws_poll_connection_closed(esp_transport_handle_t t, int timeout_ms) { - struct timeval timeout; - int sock = esp_transport_get_socket(t); - fd_set readset; - fd_set errset; - FD_ZERO(&readset); - FD_ZERO(&errset); - FD_SET(sock, &readset); - FD_SET(sock, &errset); - - int ret = select(sock + 1, &readset, NULL, &errset, esp_transport_utils_ms_to_timeval(timeout_ms, &timeout)); - if (ret > 0) { - if (FD_ISSET(sock, &readset)) { - uint8_t buffer; - if (recv(sock, &buffer, 1, MSG_PEEK) <= 0) { - // socket is readable, but reads zero bytes -- connection cleanly closed by FIN flag - return 1; - } - ESP_LOGW(TAG, "esp_transport_ws_poll_connection_closed: unexpected data readable on socket=%d", sock); - } else if (FD_ISSET(sock, &errset)) { - int sock_errno = 0; - uint32_t optlen = sizeof(sock_errno); - getsockopt(sock, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen); - ESP_LOGD(TAG, "esp_transport_ws_poll_connection_closed select error %d, errno = %s, fd = %d", sock_errno, strerror(sock_errno), sock); - if (sock_errno == ENOTCONN || sock_errno == ECONNRESET || sock_errno == ECONNABORTED) { - // the three err codes above might be caused by connection termination by RTS flag - // which we still assume as expected closing sequence of ws-transport connection - return 1; - } - ESP_LOGE(TAG, "esp_transport_ws_poll_connection_closed: unexpected errno=%d on socket=%d", sock_errno, sock); - } - return -1; // indicates error: socket unexpectedly reads an actual data, or unexpected errno code - } - return ret; - + transport_ws_t *ws = esp_transport_get_context_data(t); + return esp_transport_poll_connection_closed(ws->parent, timeout_ms); }