[host] Ignore SIGPIPE so a write to a closed peer fails with EPIPE (#20228)

This commit is contained in:
Bascht74
2026-10-06 22:38:16 +11:00
committed by GitHub
parent 718b6651fe
commit 6002c28a8d
3 changed files with 99 additions and 0 deletions
+2
View File
@@ -90,6 +90,8 @@ int main(int argc, char **argv) {
// Install signal handlers for graceful shutdown (flushes preferences to disk)
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
// A write to a closed peer must fail with EPIPE, as on the embedded stacks, not kill the process
std::signal(SIGPIPE, SIG_IGN);
esphome::host::setup_preferences();
setup();
@@ -0,0 +1,40 @@
esphome:
name: host-write-after-close
host:
api:
actions:
- action: write_after_close
variables:
port: int
then:
- lambda: |-
// A second connection to the test server carries the go signal.
auto sync = socket::socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_storage addr;
socklen_t len = socket::set_sockaddr(reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr), "127.0.0.1", port);
struct timeval timeout = {10, 0};
sync->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
uint8_t go;
// The loop stays blocked, so the link cannot read the close before the writes.
if (sync->connect(reinterpret_cast<struct sockaddr *>(&addr), len) != 0 || sync->read(&go, 1) != 1) {
ESP_LOGE("test", "No go signal");
return;
}
// The first write draws a reset from the closed peer, a later one fails with EPIPE.
uint32_t start = millis();
while (id(tcp_uart_1).is_connected() && millis() - start < 5000) {
id(tcp_uart_1).write_byte('b');
id(tcp_uart_1).flush();
delay(1);
}
logger:
level: INFO
tcp_uart:
- id: tcp_uart_1
host: 127.0.0.1
port: 18126
reconnect_interval: 60s
@@ -0,0 +1,57 @@
"""A host build must survive a write to a peer that has closed the connection.
The device writes until the link drops; once the closed peer has answered with
a reset, a write must fail with EPIPE instead of SIGPIPE killing the process.
"""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_host_write_after_peer_close(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
unused_tcp_port_factory: Callable[[], int],
) -> None:
port = unused_tcp_port_factory()
yaml_config = yaml_config.replace("port: 18126", f"port: {port}")
lines = LineWaiter()
peers: asyncio.Queue[asyncio.StreamWriter] = asyncio.Queue()
async def handle(_: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
peers.put_nowait(writer)
server = await asyncio.start_server(handle, "127.0.0.1", port)
try:
async with (
run_compiled(yaml_config, line_callback=lines.callback),
api_client_connected() as client,
):
peer = await asyncio.wait_for(peers.get(), 15.0)
_, services = await client.list_entities_services()
action = next(s for s in services if s.name == "write_after_close")
await client.execute_service(action, {"port": port})
# The action opens a second connection and blocks the loop until it gets a byte.
sync = await asyncio.wait_for(peers.get(), 15.0)
peer.close()
await peer.wait_closed()
sync.write(b"g")
sync.close()
# EPIPE (32 on Linux and macOS) must come back as an errno.
await lines.wait_for("Connection lost: 32")
assert await client.device_info() is not None
finally:
server.close()
await server.wait_closed()