1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-06 20:03:46 +01:00

Merge branch 'fix_watchdog_reset_idf_web_server' into integration

This commit is contained in:
J. Nick Koston
2025-10-03 14:21:26 -05:00
3 changed files with 68 additions and 5 deletions

View File

@@ -127,6 +127,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
deferred_queue_.erase(deferred_queue_.begin());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} else {
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_()
// The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs
// fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic,
// also update the ESP-IDF implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead

View File

@@ -4,6 +4,8 @@
#include <memory>
#include <cstring>
#include <cctype>
#include <sys/socket.h>
#include <errno.h>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -46,6 +48,28 @@ DefaultHeaders default_headers_instance;
DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
namespace {
// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
if (buf == nullptr) {
return HTTPD_SOCK_ERR_INVALID;
}
// Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Buffer full - retry later
return HTTPD_SOCK_ERR_TIMEOUT;
}
// Real error
ESP_LOGD(TAG, "send error: errno %d", errno);
return HTTPD_SOCK_ERR_FAIL;
}
return ret;
}
} // namespace
void AsyncWebServer::end() {
if (this->server_) {
httpd_stop(this->server_);
@@ -384,6 +408,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
this->hd_ = req->handle;
this->fd_.store(httpd_req_to_sockfd(req));
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
// Configure reconnect timeout and send config
// this should always go through since the tcp send buffer is empty on connect
std::string message = ws->get_config_json();
@@ -459,15 +486,45 @@ void AsyncEventSourceResponse::process_buffer_() {
return;
}
int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_,
event_buffer_.size() - event_bytes_sent_, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Socket error - just return, the connection will be closed by httpd
// and our destroy callback will be called
size_t remaining = event_buffer_.size() - event_bytes_sent_;
int bytes_sent =
httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// EAGAIN/EWOULDBLOCK - socket buffer full, try again later
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
// The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
// close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
// update the Arduino implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
this->consecutive_send_failures_);
this->fd_.store(0); // Mark for cleanup
this->deferred_queue_.clear();
}
return;
}
if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Real socket error - connection will be closed by httpd and destroy callback will be called
return;
}
if (bytes_sent <= 0) {
// Unexpected error or zero bytes sent
ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
return;
}
// Successful send - reset failure counter
this->consecutive_send_failures_ = 0;
event_bytes_sent_ += bytes_sent;
// Log partial sends for debugging
if (event_bytes_sent_ < event_buffer_.size()) {
ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
event_buffer_.size());
}
if (event_bytes_sent_ == event_buffer_.size()) {
event_buffer_.resize(0);
event_bytes_sent_ = 0;

View File

@@ -283,6 +283,8 @@ class AsyncEventSourceResponse {
std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
std::string event_buffer_{""};
size_t event_bytes_sent_;
uint16_t consecutive_send_failures_{0};
static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate
};
using AsyncEventSourceClient = AsyncEventSourceResponse;