diff --git a/backend/src/utilities/interface_bridge_helpers.py b/backend/src/utilities/interface_bridge_helpers.py index 9443cd7..f2b9ac7 100644 --- a/backend/src/utilities/interface_bridge_helpers.py +++ b/backend/src/utilities/interface_bridge_helpers.py @@ -1,11 +1,15 @@ from typing import Any, Dict, List, Optional +import errno import logging import os import subprocess +import time # ---- Logging ---------------------------------------------------------- logger = logging.getLogger("af_packet_sniffer") _ETHTOOL_BIN = "/usr/sbin/ethtool" if os.path.exists("/usr/sbin/ethtool") else "ethtool" +_ETHERNET_PROFILE_CACHE_TTL_SECONDS = 3.0 +_ETHERNET_PROFILE_CACHE: Dict[str, tuple[float, Dict[str, Any]]] = {} # ------------------------- # Interface / bridge helpers @@ -39,6 +43,12 @@ def _read_sysfs_text(path: str) -> Optional[str]: return f.read().strip() except FileNotFoundError: return None + except OSError as exc: + if exc.errno in {errno.EINVAL, errno.EIO, errno.ENODEV}: + logger.debug("Transient sysfs read failure for %s: %s", path, exc) + return None + logger.exception("Error reading sysfs path %s", path) + return None except Exception: logger.exception("Error reading sysfs path %s", path) return None @@ -98,7 +108,7 @@ def read_interface_ethernet_profile(iface: str) -> Optional[Dict[str, Any]]: check=True, ) except (FileNotFoundError, subprocess.CalledProcessError): - return None + return _get_cached_ethernet_profile(iface) values: dict[str, str] = {} for line in result.stdout.splitlines(): @@ -128,13 +138,29 @@ def read_interface_ethernet_profile(iface: str) -> Optional[Dict[str, Any]]: autoneg = lowered == "on" if speed_mbps is None and duplex is None and autoneg is None: - return None + return _get_cached_ethernet_profile(iface) - return { + profile = { "speed_mbps": speed_mbps, "duplex": duplex, "autoneg": autoneg, } + _ETHERNET_PROFILE_CACHE[iface] = (time.time(), profile) + return profile + + +def _get_cached_ethernet_profile(iface: str) -> Optional[Dict[str, Any]]: + """Return a recent ethtool snapshot to smooth short renegotiation gaps.""" + cached = _ETHERNET_PROFILE_CACHE.get(iface) + if cached is None: + return None + + ts, profile = cached + if time.time() - ts > _ETHERNET_PROFILE_CACHE_TTL_SECONDS: + _ETHERNET_PROFILE_CACHE.pop(iface, None) + return None + + return dict(profile) def _read_bridge_ports_from_sysfs(bridge: str) -> List[str]: