Source code for pysnap.terminal.transport

"""TCP transport helpers for VM serial console connections."""

from __future__ import annotations

import asyncio
from collections.abc import Iterable
from contextlib import contextmanager
import socket
from time import monotonic, sleep
from typing import Iterator

from pysnap.errors import PySnapError


def _candidate_hosts(host: str) -> tuple[str, ...]:
    """Return connection candidates for one requested host value.

    macOS loopback services are not always reachable through the same textual
    loopback address as Linux, so PySnap tries a small set of equivalent
    loopback targets when a local host is requested.

    :param host: Requested host name.
    :returns: Ordered unique host candidates.
    """
    local_hosts = {"127.0.0.1", "::1", "localhost"}
    if host not in local_hosts:
        return (host,)
    return _unique_hosts((host, "localhost", "127.0.0.1", "::1"))


def _unique_hosts(hosts: Iterable[str]) -> tuple[str, ...]:
    """Return hosts in input order without duplicates.

    :param hosts: Host candidates.
    :returns: Unique host values.
    """
    ordered: list[str] = []
    seen: set[str] = set()
    for candidate in hosts:
        if candidate in seen:
            continue
        seen.add(candidate)
        ordered.append(candidate)
    return tuple(ordered)


[docs] def open_serial_socket( host: str, port: int, timeout: float = 30.0, retry_delay: float = 0.25, ) -> socket.socket: """Wait for and open a blocking TCP socket to the VM serial port. :param host: Target host name. :param port: Target TCP port. :param timeout: Maximum time to wait. :param retry_delay: Delay between connection attempts. :returns: Connected socket object. :raises PySnapError: If the TCP server does not become available in time. """ deadline = monotonic() + timeout last_error: OSError | None = None while monotonic() < deadline: remaining = max(deadline - monotonic(), 0.1) connect_timeout = min(remaining, max(retry_delay, 0.1), 1.0) for candidate_host in _candidate_hosts(host): try: connection = socket.create_connection( (candidate_host, port), timeout=connect_timeout, ) connection.settimeout(1.0) return connection except OSError as error: last_error = error sleep(retry_delay) if last_error is None: raise PySnapError(f"Timed out while waiting for serial TCP port {port}.") raise PySnapError( f"Timed out while waiting for serial TCP port {port}: {last_error}" )
[docs] @contextmanager def serial_connection_probe( host: str, port: int, timeout: float = 30.0, retry_delay: float = 0.25, ) -> Iterator[socket.socket]: """Open a short-lived serial TCP connection for automated checks. The probe writes one newline to verify that the transport accepts client output and then keeps the connection open for the caller-managed context. :param host: Target host name. :param port: Target TCP port. :param timeout: Maximum time to wait. :param retry_delay: Delay between connection attempts. :yields: Connected socket object. """ connection = open_serial_socket( host=host, port=port, timeout=timeout, retry_delay=retry_delay, ) try: connection.sendall(b"\n") yield connection finally: connection.close()
[docs] async def open_serial_connection( host: str, port: int, timeout: float = 30.0, retry_delay: float = 0.25, ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: """Wait for and open a TCP connection to the VM serial port. :param host: Target host name. :param port: Target TCP port. :param timeout: Maximum time to wait. :param retry_delay: Delay between connection attempts. :returns: Reader and writer stream objects. :raises PySnapError: If the TCP server does not become available in time. """ deadline = monotonic() + timeout last_error: OSError | None = None while monotonic() < deadline: for candidate_host in _candidate_hosts(host): try: return await asyncio.open_connection(candidate_host, port) except OSError as error: last_error = error await asyncio.sleep(retry_delay) if last_error is None: raise PySnapError(f"Timed out while waiting for serial TCP port {port}.") raise PySnapError( f"Timed out while waiting for serial TCP port {port}: {last_error}" )