hardbyte / hardbyte/python-can

`SocketcanBus` does not support socket file descriptors over 1023

Aperta
#2,053 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
bug
Lingua principale
Python
Stelle
1.6k
Fork
697
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

### Describe the bug
Python-can’s `SocketcanBus` uses Python’s `select.select()` to wait for socket read and write availability. This function is an interface to the Unix `select` system call ([ref](https://docs.python.org/3.10/library/select.html#select.select)). There is a known issue where the file descriptor limit `FD_SETSIZE` is hardcoded to 1024 in the glibc implementation ([ref](https://man7.org/linux/man-pages/man2/select.2.html#BUGS)).

This means that even on systems where the file descriptor limit is > 1023, when the socket file descriptor is > 1023, `SocketcanBus` propagates a `ValueError` raised by `select.select()` with the message, `filedescriptor out of range in select()`.

The recommended fix is to use `poll()` or `epoll()` instead.

### To Reproduce

1. Set the system file descriptor limit above 1024 (e.g., `ulimit -n 8192`)
2. Open > 1024 files to simulate a high number of open files (e.g., `for i in {1..2000}; do exec {fd}>/dev/null; done`
3. Use a `SocketcanBus` instance to send or receive a CAN message

### Expected behavior

I'd expect `SocketcanBus` to just work when socket fd > 1023

### Additional context

OS and version: Ubuntu 22.04.5 LTS
Python version: 3.10
python-can version: first seen on 4.5.0, verified also an issue on 4.6.1 with unit tests below
python-can interface/s (if applicable): SocketcanBus

Traceback and logs

Traceback from repro on python-can 4.5.0:
```
self =
msg = can.Message(timestamp=0.0, arbitration_id=0x18ebff17, is_extended_id=True, dlc=8, data=[0x1, 0x31, 0x58, 0x50, 0x42, 0x44, 0x34, 0x39])
timeout = 0
def send(self, msg: Message, timeout: Optional[float] = None) -> None:
"""Transmit a message to the CAN bus.
:param msg: A message object.
:param timeout:
Wait up to this many seconds for the transmit queue to be ready.
If not given, the call may fail immediately.
:raises ~can.exceptions.CanError:
if the message could not be written.
"""
log.debug("We've been asked to write a message to the bus")
logger_tx = log.getChild("tx")
logger_tx.debug("sending: %s", msg)
started = time.time()
# If no timeout is given, poll for availability
if timeout is None:
timeout = 0
time_left = timeout
data = build_can_frame(msg)
while time_left >= 0:
# Wait for write availability
> ready = select.select([], [self.socket], [], time_left)[1]
E ValueError: filedescriptor out of range in select()
../python_can+/can/interfaces/socketcan/socketcan.py:788: ValueError
```

Unit tests that also reproduce the error:

```python
#!/usr/bin/env python

"""
Test that SocketcanBus.send() and recv() work with file descriptors > 1023.
"""

import select
import unittest
from unittest.mock import MagicMock, patch

import can
from can import Message
from can.interfaces.socketcan.socketcan import build_can_frame

from .config import IS_LINUX

HIGH_FD = 1024

@unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux")
class TestSocketcanHighFd(unittest.TestCase):
"""Verify SocketcanBus works when the underlying socket fd exceeds 1023."""

def setUp(self):
patcher_create = patch("can.interfaces.socketcan.socketcan.create_socket")
patcher_bind = patch("can.interfaces.socketcan.socketcan.bind_socket")

self.mock_create_socket = patcher_create.start()
self.mock_bind_socket = patcher_bind.start()

self.mock_socket = MagicMock()
self.mock_socket.fileno.return_value = HIGH_FD
self.mock_create_socket.return_value = self.mock_socket

self.bus = can.Bus(interface="socketcan", channel="can0")

self.addCleanup(patcher_create.stop)
self.addCleanup(patcher_bind.stop)

def tearDown(self):
self.bus.shutdown()

def test_send_high_fd(self):
"""send() succeeds when the socket fd > 1023."""

msg = Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6, 7, 8])
frame_data = build_can_frame(msg)
self.mock_socket.send.return_value = len(frame_data)

self.bus.send(msg)

self.mock_socket.send.assert_called_once_with(frame_data)

@patch("can.interfaces.socketcan.socketcan.capture_message")
def test_recv_high_fd(self, mock_capture):
"""recv() succeeds when the socket fd > 1023."""

expected_msg = Message(
arbitration_id=0x123,
data=[1, 2, 3, 4, 5, 6, 7, 8],
channel="can0",
timestamp=1000.0,
)
mock_capture.return_value = expected_msg

msg = self.bus.recv(timeout=1.0)

self.assertIsNotNone(msg)
self.assertEqual(msg.arbitration_id, 0x123)
self.assertEqual(msg.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8]))
mock_capture.assert_called_once_with(self.mock_socket, False)

if __name__ == "__main__":
unittest.main()
```

Unit test failure output on 4.6.1:
```
FAIL [ 9.983ms] test/test_high_fd.py::TestSocketcanHighFd::test_recv_high_fd
stdout ───
stderr ───
test/test_high_fd.py:66 in test_recv_high_fd
self = , mock_capture =

63 │ │ )
64 │ │ mock_capture.return_value = expected_msg
65 │ │
❱ 66 │ │ msg = self.bus.recv(timeout=1.0)
67 │ │
68 │ │ self.assertIsNotNone(msg)
69 │ │ self.assertEqual(msg.arbitration_id, 0x123)
can/bus.py:121 in recv

118 │ │
119 │ │ while True:
120 │ │ │ # try to get a message
❱ 121 │ │ │ msg, already_filtered = self._recv_internal(timeout=time_left)
122 │ │ │
123 │ │ │ # return it, if it matches
124 │ │ │ if msg and (already_filtered or self._matches_filters(msg)):

121: in recv

can/interfaces/socketcan/socketcan.py:827 in _recv_internal
self = , timeout = 1.0

824 │ │ try:
825 │ │ │ # get all sockets that are ready (can be a list with a single value
826 │ │ │ # being self.socket or an empty list if self.socket is not ready)
❱ 827 │ │ │ ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout)
828 │ │ except OSError as error:
829 │ │ │ # something bad happened (e.g. the interface went down)
830 │ │ │ raise can.CanOperationError(

827: ValueError
❱ ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout)
E ValueError: filedescriptor out of range in select()
FAIL [ 4.712ms] test/test_high_fd.py::TestSocketcanHighFd::test_send_high_fd
stdout ───
stderr ───
test/test_high_fd.py:50 in test_send_high_fd
self =

47 │ │ frame_data = build_can_frame(msg)
48 │ │ self.mock_socket.send.return_value = len(frame_data)
49 │ │
❱ 50 │ │ self.bus.send(msg)
51 │ │
52 │ │ self.mock_socket.send.assert_called_once_with(frame_data)
53
can/interfaces/socketcan/socketcan.py:869 in send
self = , msg = can.Message(timestamp=0.0, arbitration_id=0x123,
is_extended_id=True, dlc=8, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), timeout = 0

866 │ │
867 │ │ while time_left >= 0:
868 │ │ │ # Wait for write availability
❱ 869 │ │ │ ready = select.select([], [self.socket], [], time_left)[1]
870 │ │ │ if not ready:
871 │ │ │ │ # Timeout
872 │ │ │ │ break

869: ValueError
❱ ready = select.select([], [self.socket], [], time_left)[1]
E ValueError: filedescriptor out of range in select()
```

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.