From 51f9bdc2b55ac2b73f6b6f1f94f947ecc50eece2 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Tue, 7 Apr 2026 10:46:54 -0400 Subject: [PATCH] use SO_TIMESTAMP, works on Linux, FreeBSD and macOS --- hbd/server/udp.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/hbd/server/udp.py b/hbd/server/udp.py index 20cebef..1958e8e 100644 --- a/hbd/server/udp.py +++ b/hbd/server/udp.py @@ -14,22 +14,23 @@ from . import notify as notify_mod logger = logging.getLogger(__name__) eventlog = notify_mod.eventlog -# SO_TIMESTAMPNS: kernel attaches a struct timespec to each received datagram. -# The constant is not exposed by Python's socket module on all platforms, -# so fall back to the Linux value (35) when absent. -_SO_TIMESTAMPNS = getattr(socket, 'SO_TIMESTAMPNS', 35) -# struct timespec uses two native C longs: tv_sec and tv_nsec -_TIMESPEC = struct.Struct('@ll') +# SO_TIMESTAMP: kernel attaches a struct timeval to each received datagram. +# Supported on Linux, FreeBSD, and macOS. The constant is not exposed by +# Python's socket module on all platforms, so fall back to the Linux value (29) +# when absent. +_SO_TIMESTAMP = getattr(socket, 'SO_TIMESTAMP', 29) +# struct timeval uses two native C longs: tv_sec and tv_usec +_TIMEVAL = struct.Struct('@ll') def enable_kernel_timestamps(sock) -> bool: - """Try to enable SO_TIMESTAMPNS on *sock*. + """Try to enable SO_TIMESTAMP on *sock*. Returns True if the kernel will supply receive timestamps, False otherwise - (non-Linux, older kernel, or insufficient permissions). + (unsupported platform, older kernel, or insufficient permissions). """ try: - sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMPNS, 1) + sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP, 1) return True except OSError: return False @@ -38,18 +39,18 @@ def enable_kernel_timestamps(sock) -> bool: def _extract_kernel_ts(ancdata) -> float | None: """Parse recvmsg ancillary data and return the kernel receive time. - Returns seconds as a float, or None if no SO_TIMESTAMPNS cmsg is present. + Returns seconds as a float, or None if no SO_TIMESTAMP cmsg is present. """ for cmsg_level, cmsg_type, cmsg_data in ancdata: - if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMPNS: - if len(cmsg_data) >= _TIMESPEC.size: - sec, nsec = _TIMESPEC.unpack_from(cmsg_data) - return sec + nsec * 1e-9 + if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP: + if len(cmsg_data) >= _TIMEVAL.size: + sec, usec = _TIMEVAL.unpack_from(cmsg_data) + return sec + usec * 1e-6 return None class RecvmsgTransport: - """Thin wrapper used when SO_TIMESTAMPNS is active (add_reader path). + """Thin wrapper used when SO_TIMESTAMP is active (add_reader path). Exposes the same sendto() / close() interface as asyncio's DatagramTransport so the rest of the code does not need to know which path is in use. @@ -79,8 +80,8 @@ def make_recvmsg_reader(sock, handler, transport): """Return a callback suitable for loop.add_reader(). Reads one datagram per call using recvmsg() so that kernel timestamps in - the ancillary data are accessible. Falls back to time.time() gracefully - if the cmsg is missing. + the ancillary data are accessible. Falls back to time.time() if the + cmsg is missing. handler(msg, addr, transport, kernel_ts) – same signature as udp_handler in main.py with the optional kernel_ts argument.