From a64ccc50ae098741de5010f86a59187813450006 Mon Sep 17 00:00:00 2001 From: Bryan Hicks Date: Mon, 11 May 2026 23:48:25 +0000 Subject: [PATCH] Replaced select.select() with select.epoll() in SocketcanBus --- can/interfaces/socketcan/socketcan.py | 16 +++++++++++--- doc/changelog.d/2053.fixed.rst | 1 + test/test_socketcan.py | 32 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/2053.fixed.rst diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 6dc856cbf..1dab488c3 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -5,8 +5,8 @@ At the end of the file the usage of the internal methods is shown. """ +# pylint: disable=too-many-lines import ctypes -import ctypes.util import errno import logging import select @@ -786,6 +786,10 @@ def __init__( # so this is always supported by the kernel self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1) + # Create epoll instance and register socket for read and write events + self._epoll = select.epoll() + self._epoll.register(self.socket.fileno(), select.EPOLLIN | select.EPOLLOUT) + try: bind_socket(self.socket, channel) kwargs.update( @@ -810,6 +814,9 @@ def shutdown(self) -> None: for channel, bcm_socket in self._bcm_sockets.items(): log.debug("Closing bcm socket for channel %s", channel) bcm_socket.close() + if not self._epoll.closed: + self._epoll.unregister(self.socket.fileno()) + self._epoll.close() log.debug("Closing raw can socket") self.socket.close() @@ -817,7 +824,9 @@ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) - ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) + epoll_timeout = timeout if timeout is not None else -1 + events = self._epoll.poll(epoll_timeout) + ready_receive_sockets = any(event & select.EPOLLIN for _, event in events) except OSError as error: # something bad happened (e.g. the interface went down) raise can.CanOperationError( @@ -859,7 +868,8 @@ def send(self, msg: Message, timeout: float | None = None) -> None: while time_left >= 0: # Wait for write availability - ready = select.select([], [self.socket], [], time_left)[1] + events = self._epoll.poll(time_left) + ready = any(event & select.EPOLLOUT for _, event in events) if not ready: # Timeout break diff --git a/doc/changelog.d/2053.fixed.rst b/doc/changelog.d/2053.fixed.rst new file mode 100644 index 000000000..3d0f28aae --- /dev/null +++ b/doc/changelog.d/2053.fixed.rst @@ -0,0 +1 @@ +Replaced select.select() with select.epoll() in SocketcanBus diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 9d042f425..9aa3cb63f 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -5,10 +5,14 @@ """ import ctypes +import os +import resource import struct import sys +import tempfile import unittest import warnings +from contextlib import ExitStack from unittest.mock import patch import can @@ -390,6 +394,34 @@ def test_pypy_socketcan_support(self): "See: https://github.com/pypy/pypy/issues/3808" ) + @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "Only run when vcan0 is available") + @unittest.skipUnless( + resource.getrlimit(resource.RLIMIT_NOFILE)[0] > 1024, + "Only run when the system supports high file limit", + ) + def test_high_socket_fileno(self): + """send() and recv() succeed when socket fileno > 1023.""" + num_open_files = len(os.listdir("/proc/self/fd")) + num_files_to_open = 1024 - num_open_files + with ExitStack() as stack: + for _ in range(num_files_to_open): + stack.enter_context(tempfile.TemporaryFile()) + + bus1 = can.Bus(interface="socketcan", channel="vcan0") + bus2 = can.Bus(interface="socketcan", channel="vcan0") + msg = can.Message( + is_extended_id=False, + arbitration_id=0x100, + data=[1, 2, 3, 4, 5, 6, 7, 8], + ) + + timeout = 1.0 if IS_PYPY else 0.1 + bus1.send(msg) + recv_msg = bus2.recv(timeout) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.arbitration_id, 0x100) + self.assertEqual(recv_msg.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8])) + if __name__ == "__main__": unittest.main()