"""System ping helpers shared by ping_monitor and the connectivity probe.""" import asyncio import re import sys from typing import Dict, List, Optional _INF = float("inf") class PingUnavailable(Exception): """The system ping binary could not be executed.""" def ping_command(host: str, count: int, timeout: int, family: Optional[int] = None) -> List[str]: """Build the platform ping command. family is 4, 6 or None (let ping decide).""" if sys.platform == "win32": cmd = ["ping", "-n", str(count), "-w", str(timeout * 1000)] if family: cmd.append(f"-{family}") return cmd + [host] if sys.platform == "darwin" and family: if family == 6: return ["ping6", "-c", str(count), host] return ["ping", "-c", str(count), "-W", str(timeout * 1000), host] cmd = ["ping", "-c", str(count), "-W", str(timeout)] if family: cmd.append(f"-{family}") return cmd + [host] def parse_ping_output(output: str) -> Dict[str, float]: """Parse rtt_min/avg/max/loss from ping output (Linux, macOS, BSD).""" loss = 100.0 loss_match = re.search(r"(\d+(?:\.\d+)?)\s*%\s*packet\s*loss", output) if loss_match: loss = float(loss_match.group(1)) # Linux: "rtt min/avg/max/mdev = x/x/x/x ms"; macOS: "round-trip min/avg/max/stddev = ..." rtt_match = re.search( r"(?:rtt|round-trip)\s+min/avg/max/\S+\s*=\s*([\d.]+)/([\d.]+)/([\d.]+)", output, ) if rtt_match: return { "rtt_min": float(rtt_match.group(1)), "rtt_avg": float(rtt_match.group(2)), "rtt_max": float(rtt_match.group(3)), "loss": loss, } return {"rtt_min": _INF, "rtt_avg": _INF, "rtt_max": _INF, "loss": loss} async def run_ping(host: str, count: int, timeout: int, family: Optional[int] = None) -> Dict[str, float]: """Run ping and return parsed metrics. Raises PingUnavailable if the ping binary is missing and asyncio.TimeoutError (after killing the process) if it hangs. """ cmd = ping_command(host, count, timeout, family) try: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) except FileNotFoundError as e: raise PingUnavailable(str(e)) from e try: stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout * count + 2) except asyncio.TimeoutError: proc.kill() await proc.wait() raise return parse_ping_output(stdout.decode(errors="replace"))