agent_search_gateway.socket_probe
Bounded, read-only inspection of the local daemon Unix socket.
1"""Bounded, read-only inspection of the local daemon Unix socket.""" 2 3import asyncio 4import stat 5from collections.abc import Awaitable, Callable 6from contextlib import suppress 7from dataclasses import dataclass 8from enum import StrEnum 9from pathlib import Path 10 11SOCKET_PROBE_TIMEOUT_SECONDS = 2.0 12SocketConnector = Callable[..., Awaitable[tuple[asyncio.StreamReader, asyncio.StreamWriter]]] 13 14 15class SocketState(StrEnum): 16 MISSING = "missing" 17 LIVE = "live" 18 REFUSED = "refused" 19 NOT_SOCKET = "not_socket" 20 TIMEOUT = "timeout" 21 OS_ERROR = "os_error" 22 23 24@dataclass(frozen=True, slots=True) 25class SocketProbeResult: 26 state: SocketState 27 identity: tuple[int, int] | None = None 28 reason: str = "" 29 30 31async def probe_unix_socket( 32 path: Path, 33 timeout_seconds: float = SOCKET_PROBE_TIMEOUT_SECONDS, 34 connector: SocketConnector = asyncio.open_unix_connection, 35) -> SocketProbeResult: 36 try: 37 existing = path.lstat() 38 except FileNotFoundError: 39 return SocketProbeResult(SocketState.MISSING) 40 except OSError as exc: 41 return SocketProbeResult(SocketState.OS_ERROR, reason=_safe_reason(exc)) 42 43 if not stat.S_ISSOCK(existing.st_mode): 44 return SocketProbeResult(SocketState.NOT_SOCKET) 45 identity = (existing.st_dev, existing.st_ino) 46 47 try: 48 _, writer = await asyncio.wait_for( 49 connector(path=path), 50 timeout=timeout_seconds, 51 ) 52 except FileNotFoundError: 53 return SocketProbeResult(SocketState.MISSING) 54 except ConnectionRefusedError as exc: 55 return SocketProbeResult(SocketState.REFUSED, identity, _safe_reason(exc)) 56 except TimeoutError as exc: 57 return SocketProbeResult(SocketState.TIMEOUT, identity, _safe_reason(exc)) 58 except OSError as exc: 59 return SocketProbeResult(SocketState.OS_ERROR, identity, _safe_reason(exc)) 60 61 writer.close() 62 with suppress(OSError): 63 await writer.wait_closed() 64 return SocketProbeResult(SocketState.LIVE, identity) 65 66 67def _safe_reason(exc: OSError | TimeoutError) -> str: 68 text = str(exc).strip() 69 return text if text else type(exc).__name__
SOCKET_PROBE_TIMEOUT_SECONDS =
2.0
SocketConnector =
collections.abc.Callable[..., collections.abc.Awaitable[tuple[asyncio.streams.StreamReader, asyncio.streams.StreamWriter]]]
class
SocketState(enum.StrEnum):
16class SocketState(StrEnum): 17 MISSING = "missing" 18 LIVE = "live" 19 REFUSED = "refused" 20 NOT_SOCKET = "not_socket" 21 TIMEOUT = "timeout" 22 OS_ERROR = "os_error"
MISSING =
<SocketState.MISSING: 'missing'>
LIVE =
<SocketState.LIVE: 'live'>
REFUSED =
<SocketState.REFUSED: 'refused'>
NOT_SOCKET =
<SocketState.NOT_SOCKET: 'not_socket'>
TIMEOUT =
<SocketState.TIMEOUT: 'timeout'>
OS_ERROR =
<SocketState.OS_ERROR: 'os_error'>
@dataclass(frozen=True, slots=True)
class
SocketProbeResult:
25@dataclass(frozen=True, slots=True) 26class SocketProbeResult: 27 state: SocketState 28 identity: tuple[int, int] | None = None 29 reason: str = ""
SocketProbeResult( state: SocketState, identity: tuple[int, int] | None = None, reason: str = '')
state: SocketState
async def
probe_unix_socket( path: pathlib.Path, timeout_seconds: float = 2.0, connector: Callable[..., Awaitable[tuple[asyncio.streams.StreamReader, asyncio.streams.StreamWriter]]] = <function open_unix_connection>) -> SocketProbeResult:
32async def probe_unix_socket( 33 path: Path, 34 timeout_seconds: float = SOCKET_PROBE_TIMEOUT_SECONDS, 35 connector: SocketConnector = asyncio.open_unix_connection, 36) -> SocketProbeResult: 37 try: 38 existing = path.lstat() 39 except FileNotFoundError: 40 return SocketProbeResult(SocketState.MISSING) 41 except OSError as exc: 42 return SocketProbeResult(SocketState.OS_ERROR, reason=_safe_reason(exc)) 43 44 if not stat.S_ISSOCK(existing.st_mode): 45 return SocketProbeResult(SocketState.NOT_SOCKET) 46 identity = (existing.st_dev, existing.st_ino) 47 48 try: 49 _, writer = await asyncio.wait_for( 50 connector(path=path), 51 timeout=timeout_seconds, 52 ) 53 except FileNotFoundError: 54 return SocketProbeResult(SocketState.MISSING) 55 except ConnectionRefusedError as exc: 56 return SocketProbeResult(SocketState.REFUSED, identity, _safe_reason(exc)) 57 except TimeoutError as exc: 58 return SocketProbeResult(SocketState.TIMEOUT, identity, _safe_reason(exc)) 59 except OSError as exc: 60 return SocketProbeResult(SocketState.OS_ERROR, identity, _safe_reason(exc)) 61 62 writer.close() 63 with suppress(OSError): 64 await writer.wait_closed() 65 return SocketProbeResult(SocketState.LIVE, identity)