veilid/veilid-python/tests/api.py

110 lines
3.6 KiB
Python
Raw Permalink Normal View History

2023-12-15 11:30:53 -05:00
import appdirs
import errno
import os
2023-12-15 11:30:53 -05:00
import socket
import sys
import re
from collections.abc import Callable
from functools import cache
from veilid.json_api import _JsonVeilidAPI
import veilid
ERRNO_PATTERN = re.compile(r"errno (\d+)", re.IGNORECASE)
class VeilidTestConnectionError(Exception):
"""The test client could not connect to the veilid-server."""
pass
@cache
2024-04-28 12:42:13 -04:00
def server_info(subindex: int = 0) -> tuple[str, int]:
"""Return the hostname and port of the test server."""
2023-12-15 11:30:53 -05:00
VEILID_SERVER_NETWORK = os.getenv("VEILID_SERVER_NETWORK")
if VEILID_SERVER_NETWORK is None:
2024-04-28 12:42:13 -04:00
return "localhost", 5959 + subindex
2023-12-15 11:30:53 -05:00
hostname, *rest = VEILID_SERVER_NETWORK.split(":")
if rest:
2024-04-28 12:42:13 -04:00
return hostname, int(rest[0]) + subindex
return hostname, 5959 + subindex
2023-12-16 12:46:57 -05:00
def ipc_path_exists(path: str) -> bool:
"""Determine if an IPC socket exists in a platform independent way."""
if os.name == 'nt':
if not path.upper().startswith("\\\\.\\PIPE\\"):
return False
return path[9:] in os.listdir("\\\\.\\PIPE")
else:
return os.path.exists(path)
2023-12-15 11:30:53 -05:00
@cache
2024-04-28 12:42:13 -04:00
def ipc_info(subindex: int = 0) -> str:
2023-12-15 11:30:53 -05:00
"""Return the path of the ipc socket of the test server."""
VEILID_SERVER_IPC = os.getenv("VEILID_SERVER_IPC")
if VEILID_SERVER_IPC is not None:
return VEILID_SERVER_IPC
if os.name == 'nt':
2024-04-28 12:42:13 -04:00
return f'\\\\.\\PIPE\\veilid-server\\{subindex}'
2023-12-16 12:46:57 -05:00
2024-04-28 12:42:13 -04:00
ipc_path = f"/var/db/veilid-server/ipc/{subindex}"
if os.path.exists(ipc_path):
return ipc_path
2023-12-15 11:30:53 -05:00
# hack to deal with rust's 'directories' crate case-inconsistency
if sys.platform.startswith('darwin'):
2024-02-21 20:52:48 -05:00
data_dir = appdirs.user_data_dir("org.Veilid.Veilid")
2023-12-15 11:30:53 -05:00
else:
data_dir = appdirs.user_data_dir("veilid","veilid")
2024-04-28 12:42:13 -04:00
ipc_path = os.path.join(data_dir, "ipc", str(subindex))
return ipc_path
2023-12-15 11:30:53 -05:00
2024-04-28 12:42:13 -04:00
async def api_connector(callback: Callable, subindex: int = 0) -> _JsonVeilidAPI:
"""Return an API connection if possible.
If the connection fails due to an inability to connect to the
server's socket, raise an easy-to-catch VeilidTestConnectionError.
"""
2024-04-28 12:42:13 -04:00
ipc_path = ipc_info(subindex)
2023-12-15 11:30:53 -05:00
try:
2023-12-16 12:46:57 -05:00
if ipc_path_exists(ipc_path):
2023-12-15 11:30:53 -05:00
return await veilid.json_api_connect_ipc(ipc_path, callback)
else:
2024-04-28 12:42:13 -04:00
hostname, port = server_info(subindex)
2023-12-15 11:30:53 -05:00
return await veilid.json_api_connect(hostname, port, callback)
except OSError as exc:
# This is a little goofy. The underlying Python library handles
# connection errors in 2 ways, depending on how many connections
# it attempted to make:
#
# - If it only tried to connect to one IP address socket, the
# library propagates the one single OSError it got.
#
# - If it tried to connect to multiple sockets, perhaps because
# the hostname resolved to several addresses (e.g. "localhost"
# => 127.0.0.1 and ::1), then the library raises one exception
# with all the failure exception strings joined together.
# If errno is set, it's the first kind of exception. Check that
# it's the code we expected.
if exc.errno is not None:
if exc.errno == errno.ECONNREFUSED:
raise VeilidTestConnectionError
raise
# If not, use a regular expression to find all the errno values
# in the combined error string. Check that all of them have the
# code we're looking for.
errnos = ERRNO_PATTERN.findall(str(exc))
if all(int(err) == errno.ECONNREFUSED for err in errnos):
raise VeilidTestConnectionError
raise