use SO_TIMESTAMP, works on Linux, FreeBSD and macOS
This commit is contained in:
+18
-17
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user