mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[host] Ignore SIGPIPE so a write to a closed peer fails with EPIPE (#20228)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user