Move the connect poll into the socket component and dedupe target persistence

This commit is contained in:
J. Nick Koston
2026-09-03 17:41:48 +02:00
parent 1665d509d4
commit 900638463a
7 changed files with 78 additions and 65 deletions
+5 -3
View File
@@ -5,7 +5,7 @@ from typing import Any
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION
from esphome.components.const import CONF_DESCRIPTION, CONF_HOST
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
@@ -132,7 +132,6 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_EXAMPLE = "example"
CONF_HOST = "host"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
@@ -480,7 +479,10 @@ def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
from esphome.components import socket
socket_conf = fv.full_config.get().get("socket") or {}
if socket_conf.get(socket.CONF_IMPLEMENTATION) == socket.IMPLEMENTATION_LWIP_TCP:
if (
socket_conf.get(socket.CONF_IMPLEMENTATION)
in socket.IMPLEMENTATIONS_WITHOUT_CONNECT
):
raise cv.Invalid(
"outgoing_connection is not supported with the lwip_tcp socket "
"implementation because it cannot make outgoing connections",
@@ -10,9 +10,6 @@
#include <cerrno>
#include <cstring>
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <sys/select.h>
#endif
namespace esphome::api {
@@ -90,8 +87,7 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
// A corrupt remembered value can never become dialable; forget it
// (covers an IPv6 literal left by an earlier enable_ipv6 build too)
this->saved_ = {};
this->host_persisted_ = this->target_pref_.save(&this->saved_) && global_preferences->sync();
if (!this->host_persisted_) {
if (!this->persist_target_()) {
ESP_LOGW(TAG, "Failed to clear target");
}
#endif
@@ -132,58 +128,19 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
}
this->last_poll_ = now;
int err = 0;
switch (poll_connect(*this->dial_socket_, err)) {
case ConnectPollResult::CONNECT_POLL_PENDING:
switch (socket::poll_connect(*this->dial_socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
break;
case ConnectPollResult::CONNECT_POLL_CONNECTED:
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
this->handoff_(server, now);
break;
case ConnectPollResult::CONNECT_POLL_ERROR:
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
ESP_LOGW(TAG, "Connect failed: %d", err);
this->schedule_retry_(now);
break;
}
}
ConnectPollResult poll_connect(socket::Socket &sock, int &err_out) {
int fd = sock.get_fd();
if (fd < 0 || fd >= FD_SETSIZE) {
// FD_SET on either is undefined behavior
err_out = EBADF;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
// Connect completion is a write event; the main loop only selects on reads
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
#else
// Global-scope select: the entity namespace esphome::select shadows it here
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
#endif
if (ret < 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (ret == 0 || !FD_ISSET(fd, &writefds)) {
return ConnectPollResult::CONNECT_POLL_PENDING;
}
int error = 0;
socklen_t len = sizeof(error);
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (error != 0) {
err_out = error;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
return ConnectPollResult::CONNECT_POLL_CONNECTED;
}
void OutgoingConnectionManager::handoff_(APIServer *server, uint32_t now) {
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
if (this->dialed_conn_ == nullptr) {
@@ -248,8 +205,7 @@ void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
// Use the fresh address this boot even if the flash write fails; a failed
// write is retried on the next flagged hello via host_persisted_
this->saved_ = target;
this->host_persisted_ = this->target_pref_.save(&target) && global_preferences->sync();
if (!this->host_persisted_) {
if (!this->persist_target_()) {
ESP_LOGW(TAG, "Failed to save target");
return;
}
@@ -25,18 +25,6 @@ class APIConnection;
// target is simply relearned
static constexpr size_t SAVED_TARGET_HOST_LEN = socket::SOCKADDR_STR_LEN;
enum class ConnectPollResult : uint8_t {
CONNECT_POLL_PENDING,
CONNECT_POLL_CONNECTED,
CONNECT_POLL_ERROR,
};
/// Check a non-blocking connect() for completion without blocking. On
/// CONNECT_POLL_ERROR, err_out holds the socket's SO_ERROR, or errno when the
/// poll itself failed. Lives here for now; a candidate for the socket
/// component (async_tcp has a near-duplicate poll).
ConnectPollResult poll_connect(socket::Socket &sock, int &err_out);
struct SavedOutgoingTarget {
// IP as text so the socket component's v4-mapped-IPv6 normalization is
// reused on both ends; empty = none remembered
@@ -86,6 +74,13 @@ class OutgoingConnectionManager {
void schedule_retry_(uint32_t now);
// Wait without escalating the backoff (used for unmet preconditions)
void schedule_wait_(uint32_t now, uint32_t wait);
#ifndef API_OUTGOING_CONNECTION_HOST
// Write saved_ to flash, tracking success in host_persisted_
bool persist_target_() {
this->host_persisted_ = this->target_pref_.save(&this->saved_) && global_preferences->sync();
return this->host_persisted_;
}
#endif
const char *target_host_() const {
#ifdef API_OUTGOING_CONNECTION_HOST
return API_OUTGOING_CONNECTION_HOST;
+1
View File
@@ -22,6 +22,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_HOST = "host"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
+2
View File
@@ -15,6 +15,8 @@ CODEOWNERS = ["@esphome/core"]
CONF_IMPLEMENTATION = "implementation"
IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
# Implementations whose sockets cannot make outgoing connections
IMPLEMENTATIONS_WITHOUT_CONNECT = frozenset({IMPLEMENTATION_LWIP_TCP})
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
+44
View File
@@ -2,6 +2,9 @@
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
#include <cerrno>
#include <cstring>
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <sys/select.h>
#endif
#include <string>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -199,6 +202,47 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
return sizeof(sockaddr_in);
}
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
ConnectPollResult poll_connect(Socket &sock, int &err_out) {
int fd = sock.get_fd();
if (fd < 0 || fd >= FD_SETSIZE) {
// FD_SET on either is undefined behavior
err_out = EBADF;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
// Connect completion is a write event; the main loop only selects on reads
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
#else
// Global-scope select: the entity namespace esphome::select shadows it here
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
#endif
if (ret < 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (ret == 0 || !FD_ISSET(fd, &writefds)) {
return ConnectPollResult::CONNECT_POLL_PENDING;
}
int error = 0;
socklen_t len = sizeof(error);
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (error != 0) {
err_out = error;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
return ConnectPollResult::CONNECT_POLL_CONNECTED;
}
#endif
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
#if USE_NETWORK_IPV6
if (addrlen < sizeof(sockaddr_in6)) {
+13
View File
@@ -145,6 +145,19 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
enum class ConnectPollResult : uint8_t {
CONNECT_POLL_PENDING,
CONNECT_POLL_CONNECTED,
CONNECT_POLL_ERROR,
};
/// Check a non-blocking connect() for completion without blocking. On
/// CONNECT_POLL_ERROR, err_out holds the socket's SO_ERROR, or errno when the
/// poll itself failed.
ConnectPollResult poll_connect(Socket &sock, int &err_out);
#endif
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);