From f2a3274849b84de0bd6e18cd68f8147e3223ab19 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 19:52:28 +0100 Subject: [PATCH 01/11] support windows Neither the SO_TIMESTAMPNS / via recvmsg() method, nor the SIOCGSTAMP / ioctl() method for obtaining the packet timestamp is supported on Windows. This code adds a fallback to datetime, and switches to recvfrom() so that the udp_multicast bus becomes usable on windows. --- can/interfaces/udp_multicast/bus.py | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 8ca2d516b..2d4410ce2 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -4,6 +4,8 @@ import socket import struct import warnings +import sys +from datetime import datetime from typing import List, Optional, Tuple, Union import can @@ -12,9 +14,12 @@ from .utils import check_msgpack_installed, pack_message, unpack_message +ioctl_not_supported = False + try: from fcntl import ioctl except ModuleNotFoundError: # Missing on Windows + ioctl_not_supported = True pass @@ -272,7 +277,9 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: try: sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) except OSError as error: - if error.errno == errno.ENOPROTOOPT: # It is unavailable on macOS + if ( + error.errno == errno.ENOPROTOOPT or error.errno == errno.EINVAL + ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL) self.timestamp_nanosecond = False else: raise error @@ -353,18 +360,18 @@ def recv( ) from exc if ready_receive_sockets: # not empty - # fetch data & source address - ( - raw_message_data, - ancillary_data, - _, # flags - sender_address, - ) = self._socket.recvmsg( - self.max_buffer, self.received_ancillary_buffer_size - ) - # fetch timestamp; this is configured in _create_socket() if self.timestamp_nanosecond: + # fetch data, timestamp & source address + ( + raw_message_data, + ancillary_data, + _, # flags + sender_address, + ) = self._socket.recvmsg( + self.max_buffer, self.received_ancillary_buffer_size + ) + # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message() if len(ancillary_data) != 1: raise can.CanOperationError( @@ -385,14 +392,28 @@ def recv( ) timestamp = seconds + nanoseconds * 1.0e-9 else: - result_buffer = ioctl( - self._socket.fileno(), - SIOCGSTAMP, - bytes(self.received_timestamp_struct_size), - ) - seconds, microseconds = struct.unpack( - self.received_timestamp_struct, result_buffer + # fetch data & source address + (raw_message_data, sender_address) = self._socket.recvfrom( + self.max_buffer ) + + if not ioctl_not_supported: + result_buffer = ioctl( + self._socket.fileno(), + SIOCGSTAMP, + bytes(self.received_timestamp_struct_size), + ) + seconds, microseconds = struct.unpack( + self.received_timestamp_struct, result_buffer + ) + else: + # fallback to datetime + now = datetime.now() + + # Extract seconds and microseconds + seconds = now.second + microseconds = now.microsecond + if microseconds >= 1e6: raise can.CanOperationError( f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6" From 6c896ebe7294b6b92a706f693cede0c3c8ea93f1 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 20:30:00 +0100 Subject: [PATCH 02/11] clean-up example on windows, the example would otherwise cause an _enter_buffered_busy fatal error, notifier shutdown and bus shutdown are racing eachother... --- doc/interfaces/udp_multicast.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst index 4f9745615..e41925cb3 100644 --- a/doc/interfaces/udp_multicast.rst +++ b/doc/interfaces/udp_multicast.rst @@ -53,6 +53,9 @@ from ``bus_1`` to ``bus_2``: # give the notifier enough time to get triggered by the second bus time.sleep(2.0) + # clean-up + notifier.stop() + Bus Class Documentation ----------------------- From 0e589dc7e11c8a92d1de3d1dc372691e04cdcb75 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 20:30:11 +0100 Subject: [PATCH 03/11] fix double inversion --- can/interfaces/udp_multicast/bus.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 2d4410ce2..982e234b0 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -14,12 +14,12 @@ from .utils import check_msgpack_installed, pack_message, unpack_message -ioctl_not_supported = False +ioctl_supported = True try: from fcntl import ioctl except ModuleNotFoundError: # Missing on Windows - ioctl_not_supported = True + ioctl_supported = False pass @@ -397,7 +397,7 @@ def recv( self.max_buffer ) - if not ioctl_not_supported: + if ioctl_supported: result_buffer = ioctl( self._socket.fileno(), SIOCGSTAMP, From 55e1a3d8df1f564ac9056ebc07b6a9f17b22ba65 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 21:01:34 +0100 Subject: [PATCH 04/11] remove unused import --- can/interfaces/udp_multicast/bus.py | 1 - 1 file changed, 1 deletion(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 982e234b0..7403c824d 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -4,7 +4,6 @@ import socket import struct import warnings -import sys from datetime import datetime from typing import List, Optional, Tuple, Union From 0b72ca359648657d27298b33d9c8f8397d15d201 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 21:39:55 +0100 Subject: [PATCH 05/11] datetime to time.time() --- can/interfaces/udp_multicast/bus.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 7403c824d..b7af8d151 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -3,8 +3,8 @@ import select import socket import struct +import time import warnings -from datetime import datetime from typing import List, Optional, Tuple, Union import can @@ -406,12 +406,12 @@ def recv( self.received_timestamp_struct, result_buffer ) else: - # fallback to datetime - now = datetime.now() + # fallback to time.time_ns + now = time.time() # Extract seconds and microseconds - seconds = now.second - microseconds = now.microsecond + seconds = int(now) + microseconds = int((now - seconds) * 1000000) if microseconds >= 1e6: raise can.CanOperationError( From a25212e3b4ebdba19589bcfb45881b7c367a1988 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 21:43:00 +0100 Subject: [PATCH 06/11] add msgpack dependency for windows --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2b6ac04f..8fec960f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.1.0; platform_system != 'Windows'", + "msgpack~=1.1.0", ] requires-python = ">=3.8" license = { text = "LGPL v3" } From ed0ff872054f30c60f191b28226610a626bc08e6 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 21:45:00 +0100 Subject: [PATCH 07/11] enable udp_multicast back2back tests on windows --- test/back2back_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 90cf8a9bf..58a057fae 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -21,6 +21,7 @@ IS_PYPY, IS_TRAVIS, IS_UNIX, + IS_WINDOWS, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) @@ -303,8 +304,8 @@ class BasicTestSocketCan(Back2BackTestCase): # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 @unittest.skipUnless( - IS_UNIX and not (IS_CI and IS_OSX), - "only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)", + (IS_UNIX and not (IS_CI and IS_OSX)) or IS_WINDOWS, + "only supported on Unix and Windows systems (but not on macOS at Travis CI and GitHub Actions)", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" @@ -320,8 +321,8 @@ def test_unique_message_instances(self): # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 @unittest.skipUnless( - IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)), - "only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)", + (IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX))) or IS_WINDOWS, + "only supported on Unix and Windows systems (but not on Travis CI; and not on macOS at GitHub Actions)", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" From 6d6ed34cb1cb87fdbd6b89dbf525065ac51a117e Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Mon, 27 Jan 2025 22:06:42 +0100 Subject: [PATCH 08/11] handle WSAEINVAL like EINVAL --- can/interfaces/udp_multicast/bus.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index b7af8d151..e848e65a4 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -277,8 +277,10 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) except OSError as error: if ( - error.errno == errno.ENOPROTOOPT or error.errno == errno.EINVAL - ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL) + error.errno == errno.ENOPROTOOPT + or error.errno == errno.EINVAL + or error.errno == errno.WSAEINVAL + ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL) self.timestamp_nanosecond = False else: raise error From 364183a5f2d9198581dccb36ae9f4b1a80b178d7 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Sun, 2 Feb 2025 21:43:19 +0100 Subject: [PATCH 09/11] avoid potential AttributeError on Linux --- can/interfaces/udp_multicast/bus.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index e848e65a4..dd114278c 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -34,6 +34,9 @@ SO_TIMESTAMPNS = 35 SIOCGSTAMP = 0x8906 +# Additional constants for the interaction with the Winsock API +WSAEINVAL = 10022 + class UdpMulticastBus(BusABC): """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP. @@ -279,7 +282,7 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: if ( error.errno == errno.ENOPROTOOPT or error.errno == errno.EINVAL - or error.errno == errno.WSAEINVAL + or error.errno == WSAEINVAL ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL) self.timestamp_nanosecond = False else: From e6902798a74d87883dd357859a3b6c6a4c2779c3 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Sun, 2 Feb 2025 21:44:17 +0100 Subject: [PATCH 10/11] simplify unittest skip condition --- test/back2back_test.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 58a057fae..f76591d19 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -303,9 +303,8 @@ class BasicTestSocketCan(Back2BackTestCase): # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 -@unittest.skipUnless( - (IS_UNIX and not (IS_CI and IS_OSX)) or IS_WINDOWS, - "only supported on Unix and Windows systems (but not on macOS at Travis CI and GitHub Actions)", +@unittest.skipIf(IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" @@ -320,9 +319,8 @@ def test_unique_message_instances(self): # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 -@unittest.skipUnless( - (IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX))) or IS_WINDOWS, - "only supported on Unix and Windows systems (but not on Travis CI; and not on macOS at GitHub Actions)", +@unittest.skipIf(IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" From 28d62827aa82cff0843b64e7cee32262649d1f06 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Thu, 13 Feb 2025 07:41:22 +0100 Subject: [PATCH 11/11] fix formatting issue --- test/back2back_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index f76591d19..fc630fb65 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -303,7 +303,8 @@ class BasicTestSocketCan(Back2BackTestCase): # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 -@unittest.skipIf(IS_CI and IS_OSX, +@unittest.skipIf( + IS_CI and IS_OSX, "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): @@ -319,7 +320,8 @@ def test_unique_message_instances(self): # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 -@unittest.skipIf(IS_CI and IS_OSX, +@unittest.skipIf( + IS_CI and IS_OSX, "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase):