2023-12-15 11:30:53 -05:00
|
|
|
import appdirs
|
2023-07-21 19:54:32 -04:00
|
|
|
import errno
|
|
|
|
import os
|
2023-12-15 11:30:53 -05:00
|
|
|
import socket
|
|
|
|
import sys
|
2023-07-21 19:54:32 -04:00
|
|
|
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]:
|
2023-07-21 19:54:32 -04:00
|
|
|
"""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-07-21 19:54:32 -04:00
|
|
|
|
2023-12-15 11:30:53 -05:00
|
|
|
hostname, *rest = VEILID_SERVER_NETWORK.split(":")
|
2023-07-21 19:54:32 -04:00
|
|
|
if rest:
|
2024-04-28 12:42:13 -04:00
|
|
|
return hostname, int(rest[0]) + subindex
|
|
|
|
return hostname, 5959 + subindex
|
2023-07-21 19:54:32 -04:00
|
|
|
|
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
|
|
|
|
2023-07-21 19:54:32 -04:00
|
|
|
|
2024-04-28 12:42:13 -04:00
|
|
|
async def api_connector(callback: Callable, subindex: int = 0) -> _JsonVeilidAPI:
|
2023-07-21 19:54:32 -04:00
|
|
|
"""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
|
|
|
|
2023-07-21 19:54:32 -04: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)
|
2023-07-21 19:54:32 -04:00
|
|
|
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
|