Skip to content

Commit 19cf49a

Browse files
authored
Fix timestamp handling in udp_multicast on macOS (#1278)
* Fix timestamp handling in udp_multicast on macOS * Fix import on Windows * Fix test conditions for BasicTestUdpMulticastBusIPv4/6 * Fix attribute init * Fix exceptions (don't use asserts, always raise CAN errors)
1 parent 638d81a commit 19cf49a

File tree

3 files changed

+76
-27
lines changed

3 files changed

+76
-27
lines changed

can/interfaces/udp_multicast/bus.py

+72-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import errno
12
import logging
23
import select
34
import socket
45
import struct
56

7+
try:
8+
from fcntl import ioctl
9+
except ModuleNotFoundError: # Missing on Windows
10+
pass
11+
612
from typing import List, Optional, Tuple, Union
713

814
log = logging.getLogger(__name__)
@@ -21,6 +27,7 @@
2127

2228
# Additional constants for the interaction with Unix kernels
2329
SO_TIMESTAMPNS = 35
30+
SIOCGSTAMP = 0x8906
2431

2532

2633
class UdpMulticastBus(BusABC):
@@ -174,6 +181,9 @@ def __init__(
174181
self.hop_limit = hop_limit
175182
self.max_buffer = max_buffer
176183

184+
# `False` will always work, no matter the setup. This might be changed by _create_socket().
185+
self.timestamp_nanosecond = False
186+
177187
# Look up multicast group address in name server and find out IP version of the first suitable target
178188
# and then get the address family of it (socket.AF_INET or socket.AF_INET6)
179189
connection_candidates = socket.getaddrinfo( # type: ignore
@@ -200,8 +210,15 @@ def __init__(
200210

201211
# used in recv()
202212
self.received_timestamp_struct = "@ll"
203-
ancillary_data_size = struct.calcsize(self.received_timestamp_struct)
204-
self.received_ancillary_buffer_size = socket.CMSG_SPACE(ancillary_data_size)
213+
self.received_timestamp_struct_size = struct.calcsize(
214+
self.received_timestamp_struct
215+
)
216+
if self.timestamp_nanosecond:
217+
self.received_ancillary_buffer_size = socket.CMSG_SPACE(
218+
self.received_timestamp_struct_size
219+
)
220+
else:
221+
self.received_ancillary_buffer_size = 0
205222

206223
# used by send()
207224
self._send_destination = (self.group, self.port)
@@ -238,7 +255,15 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket:
238255
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
239256

240257
# set how to receive timestamps
241-
sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
258+
try:
259+
sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
260+
except OSError as error:
261+
if error.errno == errno.ENOPROTOOPT: # It is unavailable on macOS
262+
self.timestamp_nanosecond = False
263+
else:
264+
raise error
265+
else:
266+
self.timestamp_nanosecond = True
242267

243268
# Bind it to the port (on any interface)
244269
sock.bind(("", self.port))
@@ -272,18 +297,22 @@ def send(self, data: bytes, timeout: Optional[float] = None) -> None:
272297
273298
:param timeout: the timeout in seconds after which an Exception is raised is sending has failed
274299
:param data: the data to be sent
275-
:raises OSError: if an error occurred while writing to the underlying socket
276-
:raises socket.timeout: if the timeout ran out before sending was completed (this is a subclass of
277-
*OSError*)
300+
:raises can.CanOperationError: if an error occurred while writing to the underlying socket
301+
:raises can.CanTimeoutError: if the timeout ran out before sending was completed
278302
"""
279303
if timeout != self._last_send_timeout:
280304
self._last_send_timeout = timeout
281305
# this applies to all blocking calls on the socket, but sending is the only one that is blocking
282306
self._socket.settimeout(timeout)
283307

284-
bytes_sent = self._socket.sendto(data, self._send_destination)
285-
if bytes_sent < len(data):
286-
raise socket.timeout()
308+
try:
309+
bytes_sent = self._socket.sendto(data, self._send_destination)
310+
if bytes_sent < len(data):
311+
raise TimeoutError()
312+
except TimeoutError:
313+
raise can.CanTimeoutError() from None
314+
except OSError as error:
315+
raise can.CanOperationError("failed to send via socket") from error
287316

288317
def recv(
289318
self, timeout: Optional[float] = None
@@ -320,21 +349,41 @@ def recv(
320349
self.max_buffer, self.received_ancillary_buffer_size
321350
)
322351

323-
# fetch timestamp; this is configured in in _create_socket()
324-
assert len(ancillary_data) == 1, "only requested a single extra field"
325-
cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
326-
assert (
327-
cmsg_level == socket.SOL_SOCKET and cmsg_type == SO_TIMESTAMPNS
328-
), "received control message type that was not requested"
329-
# see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
330-
seconds, nanoseconds = struct.unpack(
331-
self.received_timestamp_struct, cmsg_data
332-
)
333-
if nanoseconds >= 1e9:
334-
raise can.CanError(
335-
f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
352+
# fetch timestamp; this is configured in _create_socket()
353+
if self.timestamp_nanosecond:
354+
# Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message()
355+
if len(ancillary_data) != 1:
356+
raise can.CanOperationError(
357+
"Only requested a single extra field but got a different amount"
358+
)
359+
cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
360+
if cmsg_level != socket.SOL_SOCKET or cmsg_type != SO_TIMESTAMPNS:
361+
raise can.CanOperationError(
362+
"received control message type that was not requested"
363+
)
364+
# see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
365+
seconds, nanoseconds = struct.unpack(
366+
self.received_timestamp_struct, cmsg_data
367+
)
368+
if nanoseconds >= 1e9:
369+
raise can.CanOperationError(
370+
f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
371+
)
372+
timestamp = seconds + nanoseconds * 1.0e-9
373+
else:
374+
result_buffer = ioctl(
375+
self._socket.fileno(),
376+
SIOCGSTAMP,
377+
bytes(self.received_timestamp_struct_size),
378+
)
379+
seconds, microseconds = struct.unpack(
380+
self.received_timestamp_struct, result_buffer
336381
)
337-
timestamp = seconds + nanoseconds * 1.0e-9
382+
if microseconds >= 1e6:
383+
raise can.CanOperationError(
384+
f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6"
385+
)
386+
timestamp = seconds + microseconds * 1e-6
338387

339388
return raw_message_data, sender_address, timestamp
340389

doc/interfaces/udp_multicast.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ for specifying multicast IP addresses.
2525
Supported Platforms
2626
-------------------
2727

28-
It should work on most Unix systems (including Linux with kernel 2.6.22+) but currently not on Windows.
28+
It should work on most Unix systems (including Linux with kernel 2.6.22+ and macOS) but currently not on Windows.
2929

3030
Example
3131
-------

test/back2back_test.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class BasicTestSocketCan(Back2BackTestCase):
285285
# this doesn't even work on Travis CI for macOS; for example, see
286286
# https://travis-ci.org/github/hardbyte/python-can/jobs/745389871
287287
@unittest.skipUnless(
288-
IS_UNIX and not IS_OSX,
288+
IS_UNIX and not (IS_CI and IS_OSX),
289289
"only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)",
290290
)
291291
class BasicTestUdpMulticastBusIPv4(Back2BackTestCase):
@@ -303,8 +303,8 @@ def test_unique_message_instances(self):
303303
# this doesn't even work for loopback multicast addresses on Travis CI; for example, see
304304
# https://travis-ci.org/github/hardbyte/python-can/builds/745065503
305305
@unittest.skipUnless(
306-
IS_UNIX and not (IS_TRAVIS or IS_OSX),
307-
"only supported on Unix systems (but not on Travis CI; and not an macOS at GitHub Actions)",
306+
IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)),
307+
"only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)",
308308
)
309309
class BasicTestUdpMulticastBusIPv6(Back2BackTestCase):
310310

0 commit comments

Comments
 (0)