From 25865994cb6e6d0d1b3fbb39cdbe3a7222f04496 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Tue, 14 Apr 2026 08:56:01 -0700 Subject: [PATCH] device: Treat empty hidraw read as device removal (EOF) (#3174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Treat empty hidraw read as device removal (EOF) When select() reports a hidraw fd as readable but os.read() returns empty bytes, that's EOF per POSIX — the device has been removed. Previously this was silently treated as no data, causing the listener to loop indefinitely on a gone device instead of cleaning up. * Fix test_ping_errors missing mocks for _read_input_buffer and write The test was not mocking _read_input_buffer or write, so ping() would call into real hidapi.read() with a fake handle (fd 1). The empty-read EOF detection added in the previous commit made this consistently fail by raising OSError → NoReceiver before reaching the mocked _read path. Add the same mocks used by the adjacent test_request_errors. --------- Co-authored-by: Peter F. Patel-Schneider --- lib/hidapi/udev_impl.py | 2 ++ tests/logitech_receiver/test_base.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/hidapi/udev_impl.py b/lib/hidapi/udev_impl.py index 955dd802..4b5a6168 100644 --- a/lib/hidapi/udev_impl.py +++ b/lib/hidapi/udev_impl.py @@ -409,6 +409,8 @@ def read(device_handle, bytes_count, timeout_ms=-1): data = os.read(device_handle, bytes_count) assert data is not None assert isinstance(data, bytes), (repr(data), type(data)) + if not data: # empty read when select() said readable means EOF (device removed) + raise OSError(errno.EIO, f"device disconnected on file descriptor {int(device_handle)}") return data else: return b"" diff --git a/tests/logitech_receiver/test_base.py b/tests/logitech_receiver/test_base.py index b23f9e7f..a02696fd 100644 --- a/tests/logitech_receiver/test_base.py +++ b/tests/logitech_receiver/test_base.py @@ -188,6 +188,8 @@ def test_ping_errors(simulated_error: Hidpp10Error, expected_result): with mock.patch( "logitech_receiver.base._read", return_value=(HIDPP_SHORT_MESSAGE_ID, device_number, b"\x8f" + reply_data_sw_id + bytes([simulated_error])), + ), mock.patch("logitech_receiver.base._read_input_buffer"), mock.patch( + "logitech_receiver.base.write", return_value=None ), mock.patch("logitech_receiver.base._get_next_sw_id", return_value=next_sw_id): if isinstance(expected_result, type) and issubclass(expected_result, Exception): with pytest.raises(expected_result) as context: