Compare commits

..

11 Commits

Author SHA1 Message Date
J. Nick Koston
419ea723b8 Merge remote-tracking branch 'upstream/dev' into api-server-extract-accept
# Conflicts:
#	esphome/components/api/api_server.cpp
2026-02-10 12:49:40 -06:00
J. Nick Koston
a671f6ea85 Use if/else instead of continue in client loop 2026-02-09 20:42:25 -06:00
J. Nick Koston
0c62781539 Extract remove_client_() from APIServer::loop() hot path 2026-02-09 20:42:09 -06:00
J. Nick Koston
e6c743ea67 [api] Extract accept_new_connections_() from APIServer::loop() hot path 2026-02-09 20:34:11 -06:00
J. Nick Koston
4c006d98af Merge remote-tracking branch 'upstream/dev' into peername_no_double_ram
# Conflicts:
#	esphome/components/api/api_connection.cpp
2026-02-09 18:38:02 -06:00
J. Nick Koston
c08726036e Merge branch 'dev' into peername_no_double_ram 2026-01-30 20:13:13 -06:00
J. Nick Koston
d602a2e5e4 compile tmie safety at higheer level 2026-01-26 08:44:06 -10:00
J. Nick Koston
dcab12adae isra 2026-01-25 20:03:44 -10:00
J. Nick Koston
fb714636e3 missed 2026-01-25 20:02:46 -10:00
J. Nick Koston
05a431ea54 fixup 2026-01-25 20:02:46 -10:00
J. Nick Koston
1a34b4e7d7 [api] Remove duplicate peername storage to save RAM 2026-01-25 18:17:47 -10:00
9 changed files with 142 additions and 108 deletions

View File

@@ -117,37 +117,7 @@ void APIServer::setup() {
void APIServer::loop() { void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections // Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) { if (this->socket_ && this->socket_->ready()) {
while (true) { this->accept_new_connections_();
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
} }
if (this->clients_.empty()) { if (this->clients_.empty()) {
@@ -178,46 +148,84 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) { while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index]; auto &client = this->clients_[client_index];
if (!client->flags_.remove) { if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
// Common case: process active client // Common case: process active client
client->loop(); client->loop();
client_index++; client_index++;
}
}
}
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue; continue;
} }
// Rare case: handle disconnection ESP_LOGD(TAG, "Accept %s", peername);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER auto *conn = new APIConnection(std::move(sock), this);
// Save client info before closing socket and removal for the trigger this->clients_.emplace_back(conn);
char peername_buf[socket::SOCKADDR_STR_LEN]; conn->start();
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername) // First client connected - clear warning and update timestamp
client->helper_->close(); if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time(); this->last_connected_ = App.get_loop_component_start_time();
} }
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
} }
} }

View File

@@ -234,6 +234,11 @@ class APIServer : public Component,
#endif #endif
protected: protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active); const psk_t &active_psk, bool make_active);

View File

@@ -16,13 +16,19 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket { class BSDSocketImpl final : public Socket {
public: public:
BSDSocketImpl(int fd, bool monitor_loop = false) { BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
this->fd_ = fd; #ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested // Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) { if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds // Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_); this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
} }
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
} }
~BSDSocketImpl() override { ~BSDSocketImpl() override {
if (!this->closed_) { if (!this->closed_) {
@@ -46,10 +52,12 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override { int close() override {
if (!this->closed_) { if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored // Unregister from select() before closing if monitored
if (this->loop_monitored_) { if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_); App.unregister_socket_fd(this->fd_);
} }
#endif
int ret = ::close(this->fd_); int ret = ::close(this->fd_);
this->closed_ = true; this->closed_ = true;
return ret; return ret;
@@ -122,6 +130,23 @@ class BSDSocketImpl final : public Socket {
::fcntl(this->fd_, F_SETFL, fl); ::fcntl(this->fd_, F_SETFL, fl);
return 0; return 0;
} }
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
}; };
// Helper to create a socket with optional monitoring // Helper to create a socket with optional monitoring

View File

@@ -452,8 +452,6 @@ class LWIPRawImpl : public Socket {
errno = ENOSYS; errno = ENOSYS;
return -1; return -1;
} }
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) final { int setblocking(bool blocking) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
@@ -578,8 +576,6 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
} }
bool ready() const override { return this->accepted_socket_count_ > 0; }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override { std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = EBADF; errno = EBADF;

View File

@@ -11,13 +11,19 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket { class LwIPSocketImpl final : public Socket {
public: public:
LwIPSocketImpl(int fd, bool monitor_loop = false) { LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
this->fd_ = fd; #ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested // Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) { if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds // Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_); this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
} }
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
} }
~LwIPSocketImpl() override { ~LwIPSocketImpl() override {
if (!this->closed_) { if (!this->closed_) {
@@ -43,10 +49,12 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override { int close() override {
if (!this->closed_) { if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored // Unregister from select() before closing if monitored
if (this->loop_monitored_) { if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_); App.unregister_socket_fd(this->fd_);
} }
#endif
int ret = lwip_close(this->fd_); int ret = lwip_close(this->fd_);
this->closed_ = true; this->closed_ = true;
return ret; return ret;
@@ -89,6 +97,23 @@ class LwIPSocketImpl final : public Socket {
lwip_fcntl(this->fd_, F_SETFL, fl); lwip_fcntl(this->fd_, F_SETFL, fl);
return 0; return 0;
} }
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
}; };
// Helper to create a socket with optional monitoring // Helper to create a socket with optional monitoring

View File

@@ -10,10 +10,6 @@ namespace esphome::socket {
Socket::~Socket() {} Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
#endif
// Platform-specific inet_ntop wrappers // Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP) #if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

@@ -63,29 +63,13 @@ class Socket {
virtual int setblocking(bool blocking) = 0; virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; }; virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported) /// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build. virtual int get_fd() const { return -1; }
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read /// Check if socket has data ready to read
/// For select()-based sockets: non-virtual, checks Application's select() results /// For loop-monitored sockets, checks with the Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state /// For non-monitored sockets, always returns true (assumes data may be available)
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; } virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
}; };
/// Create a socket of the given domain, type and protocol. /// Create a socket of the given domain, type and protocol.

View File

@@ -605,6 +605,15 @@ void Application::unregister_socket_fd(int fd) {
} }
} }
bool Application::is_socket_ready(int fd) const {
// This function is thread-safe for reading the result of select()
// However, it should only be called after select() has been executed in the main loop
// The read_fds_ is only modified by select() in the main loop
if (fd < 0 || fd >= FD_SETSIZE)
return false;
return FD_ISSET(fd, &this->read_fds_);
}
#endif #endif
void Application::yield_with_select_(uint32_t delay_ms) { void Application::yield_with_select_(uint32_t delay_ms) {

View File

@@ -101,10 +101,6 @@
#include "esphome/components/update/update_entity.h" #include "esphome/components/update/update_entity.h"
#endif #endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome { namespace esphome {
// Teardown timeout constant (in milliseconds) // Teardown timeout constant (in milliseconds)
@@ -495,8 +491,7 @@ class Application {
void unregister_socket_fd(int fd); void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking /// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run /// This function is thread-safe for reading, but should be called after select() has run
/// The read_fds_ is only modified by select() in the main loop bool is_socket_ready(int fd) const;
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE #ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task /// Wake the main event loop from a FreeRTOS task
@@ -508,15 +503,6 @@ class Application {
protected: protected:
friend Component; friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
void register_component_(Component *comp); void register_component_(Component *comp);