Make UDS + `SIGUSR1` optional for Windows

Windows (and any CPython that doesn't expose `socket.AF_UNIX`)
can't import the UDS transport backend nor `signal.SIGUSR1`, so
the unconditional imports break `import tractor` outright on
those hosts. Guard the platform-specific bits behind capability
probes and fall back to a TCP-only runtime when the UDS backend
is unavailable.

- across `discovery/_addr.py`, `ipc/_server.py` and
  `ipc/_types.py`, gate on `getattr(socket, 'AF_UNIX', None)` +
  `platform.system()` and import `UDSAddress` /
  `MsgpackUDSStream` only when supported, leaving the names as
  `None` otherwise.
- register the `'uds'` key in `_address_types`, its
  default-loopback addr, and the transport lookup maps only when
  the backend actually loads, so TCP keeps working standalone.
- in `devx/_stackscope.py`, import `SIGUSR1` conditionally and
  set it to `None` on Windows.

Rebased onto the post-reorg tree where `_addr.py` now lives
under `tractor/discovery/`; adapt the relocated imports to the
package's `..ipc._uds` / `..ipc._tcp` paths (the original
single-dot paths would silently disable UDS on POSIX) and drop a
duplicated `TYPE_CHECKING` block and dead `import logging` left
by the move.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Gud Boi 2026-06-29 19:18:59 -04:00
parent f8b7233b3e
commit 2743b81b71
4 changed files with 130 additions and 63 deletions

View File

@ -31,12 +31,22 @@ from threading import (
RLock, RLock,
) )
import multiprocessing as mp import multiprocessing as mp
import platform
import signal
from signal import ( from signal import (
signal, signal,
getsignal, getsignal,
SIGUSR1,
SIGINT, SIGINT,
) )
if platform.system() != "Windows":
from signal import SIGUSR1
else:
SIGUSR1 = None
# import traceback # import traceback
from types import ModuleType from types import ModuleType
from typing import ( from typing import (

View File

@ -22,6 +22,10 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
) )
import platform
import socket
from bidict import bidict from bidict import bidict
from trio import ( from trio import (
SocketListener, SocketListener,
@ -32,7 +36,6 @@ from ..runtime._state import (
_def_tpt_proto, _def_tpt_proto,
) )
from ..ipc._tcp import TCPAddress from ..ipc._tcp import TCPAddress
from ..ipc._uds import UDSAddress
if TYPE_CHECKING: if TYPE_CHECKING:
from ..runtime._runtime import Actor from ..runtime._runtime import Actor
@ -40,6 +43,20 @@ if TYPE_CHECKING:
log = get_logger() log = get_logger()
HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None
IS_WINDOWS = platform.system() == "Windows"
UDSAddress = None # so references exist but do nothing on Windows
if HAS_AF_UNIX and not IS_WINDOWS:
try:
from ..ipc._uds import UDSAddress as _UDSAddress
UDSAddress = _UDSAddress
except Exception as e:
log.warning("UDS backend import failed: %s", e)
else:
log.warning("UDS backend disabled on this platform.")
# TODO, maybe breakout the netns key to a struct? # TODO, maybe breakout the netns key to a struct?
# class NetNs(Struct)[str, int]: # class NetNs(Struct)[str, int]:
# ... # ...
@ -172,20 +189,25 @@ class Address(Protocol):
_address_types: bidict[str, Type[Address]] = { _address_types: bidict[str, Type[Address]] = {
'tcp': TCPAddress, 'tcp': TCPAddress,
'uds': UDSAddress
} }
if UDSAddress is not None:
_address_types['uds'] = UDSAddress
else:
log.warning("Skipping UDS address type: no UDS backend available.")
# TODO! really these are discovery sys default addrs ONLY useful for # TODO! really these are discovery sys default addrs ONLY useful for
# when none is provided to a root actor on first boot. # when none is provided to a root actor on first boot.
_default_lo_addrs: dict[ _default_lo_addrs: dict[str, UnwrappedAddress] = {
str,
UnwrappedAddress
] = {
'tcp': TCPAddress.get_root().unwrap(), 'tcp': TCPAddress.get_root().unwrap(),
'uds': UDSAddress.get_root().unwrap(),
} }
if UDSAddress is not None:
_default_lo_addrs['uds'] = UDSAddress.get_root().unwrap()
else:
log.warning("Skipping UDS default loopback address: no UDS backend available.")
def get_address_cls(name: str) -> Type[Address]: def get_address_cls(name: str) -> Type[Address]:
return _address_types[name] return _address_types[name]

View File

@ -27,6 +27,8 @@ from functools import partial
from itertools import chain from itertools import chain
import inspect import inspect
import textwrap import textwrap
import platform
import socket
from types import ( from types import (
ModuleType, ModuleType,
) )
@ -62,16 +64,23 @@ from .. import log
from ..discovery._addr import Address from ..discovery._addr import Address
from ._chan import Channel from ._chan import Channel
from ._transport import MsgTransport from ._transport import MsgTransport
from ._uds import UDSAddress
from ._tcp import TCPAddress
if TYPE_CHECKING: if TYPE_CHECKING:
from ..runtime._runtime import Actor from ..runtime._runtime import Actor
from ..runtime._supervise import ActorNursery from ..runtime._supervise import ActorNursery
from ._tcp import TCPAddress
log = log.get_logger() log = log.get_logger()
UDSAddress = None
if getattr(socket, "AF_UNIX", None) is not None and platform.system() != "Windows":
from ._uds import UDSAddress
else:
pass
async def maybe_wait_on_canced_subs( async def maybe_wait_on_canced_subs(
uid: tuple[str, str], uid: tuple[str, str],

View File

@ -18,106 +18,132 @@
IPC subsys type-lookup helpers? IPC subsys type-lookup helpers?
''' '''
from typing import ( from typing import Type
Type, import platform
# TYPE_CHECKING,
)
import trio
import socket import socket
import trio
from tractor.log import get_logger
from tractor.ipc._transport import ( from tractor.ipc._transport import (
MsgTransportKey, MsgTransportKey,
MsgTransport MsgTransport,
) )
from tractor.ipc._tcp import ( from tractor.ipc._tcp import (
TCPAddress, TCPAddress,
MsgpackTCPStream, MsgpackTCPStream,
) )
from tractor.ipc._uds import (
UDSAddress,
MsgpackUDSStream,
)
# if TYPE_CHECKING: log = get_logger()
# from tractor._addr import Address
# ------------------------------------------------------------
# Optional UDS backend (Windows / some Pythons may not have AF_UNIX)
# ------------------------------------------------------------
HAS_AF_UNIX = getattr(socket, "AF_UNIX", None) is not None
IS_WINDOWS = platform.system() == "Windows"
Address = TCPAddress|UDSAddress HAS_UDS = False
UDSAddress = None # type: ignore
MsgpackUDSStream = None # type: ignore
# manually updated list of all supported msg transport types if HAS_AF_UNIX and not IS_WINDOWS:
_msg_transports = [ try:
from tractor.ipc._uds import ( # type: ignore
UDSAddress as _UDSAddress,
MsgpackUDSStream as _MsgpackUDSStream,
)
UDSAddress = _UDSAddress # type: ignore
MsgpackUDSStream = _MsgpackUDSStream # type: ignore
HAS_UDS = True
except Exception as e:
log.warning("UDS backend unavailable (%s); continuing without it.", e)
else:
if not HAS_AF_UNIX:
log.warning("AF_UNIX not exposed by this Python; disabling UDS backend.")
elif IS_WINDOWS:
# Even if the Windows kernel supports AF_UNIX, CPython may not expose it,
# and this project currently targets POSIX for the UDS backend.
log.warning("Windows detected; disabling UDS backend.")
# ------------------------------------------------------------
# Public types and transport registries
# ------------------------------------------------------------
# Address is TCP-only unless UDS is available.
if HAS_UDS:
Address = TCPAddress | UDSAddress # type: ignore
else:
Address = TCPAddress # type: ignore
# Manually updated list of all supported msg transport types
_msg_transports: list[Type[MsgTransport]] = [
MsgpackTCPStream, MsgpackTCPStream,
MsgpackUDSStream
] ]
if HAS_UDS:
_msg_transports.append(MsgpackUDSStream) # type: ignore
# Map MsgTransportKey -> transport type
# convert a MsgTransportKey to the corresponding transport type _key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = {
_key_to_transport: dict[ ("msgpack", "tcp"): MsgpackTCPStream,
MsgTransportKey,
Type[MsgTransport],
] = {
('msgpack', 'tcp'): MsgpackTCPStream,
('msgpack', 'uds'): MsgpackUDSStream,
} }
if HAS_UDS:
_key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore
# convert an Address wrapper to its corresponding transport type # Map Address wrapper -> transport type
_addr_to_transport: dict[ _addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore
Type[TCPAddress|UDSAddress],
Type[MsgTransport]
] = {
TCPAddress: MsgpackTCPStream, TCPAddress: MsgpackTCPStream,
UDSAddress: MsgpackUDSStream,
} }
if HAS_UDS:
_addr_to_transport[UDSAddress] = MsgpackUDSStream # type: ignore
# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
def transport_from_addr( def transport_from_addr(
addr: Address, addr: Address,
codec_key: str = 'msgpack', codec_key: str = "msgpack",
) -> Type[MsgTransport]: ) -> Type[MsgTransport]:
''' """
Given a destination address and a desired codec, find the Given a destination address and a desired codec, find the
corresponding `MsgTransport` type. corresponding `MsgTransport` type.
"""
'''
try: try:
return _addr_to_transport[type(addr)] return _addr_to_transport[type(addr)] # type: ignore[call-arg]
except KeyError: except KeyError:
raise NotImplementedError( raise NotImplementedError(
f'No known transport for address {repr(addr)}' f"No known transport for address {repr(addr)}"
) )
def transport_from_stream( def transport_from_stream(
stream: trio.abc.Stream, stream: trio.abc.Stream,
codec_key: str = 'msgpack' codec_key: str = "msgpack",
) -> Type[MsgTransport]: ) -> Type[MsgTransport]:
''' """
Given an arbitrary `trio.abc.Stream` and a desired codec, Given an arbitrary `trio.abc.Stream` and a desired codec,
find the corresponding `MsgTransport` type. find the corresponding `MsgTransport` type.
"""
'''
transport = None transport = None
if isinstance(stream, trio.SocketStream): if isinstance(stream, trio.SocketStream):
sock: socket.socket = stream.socket sock: socket.socket = stream.socket
match sock.family: fam = sock.family
case socket.AF_INET | socket.AF_INET6:
transport = 'tcp'
case socket.AF_UNIX: if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)):
transport = 'uds' transport = "tcp"
case _: # Only consider AF_UNIX when both Python exposes it and our backend is active
raise NotImplementedError( if transport is None and HAS_UDS and HAS_AF_UNIX and fam == socket.AF_UNIX: # type: ignore[attr-defined]
f'Unsupported socket family: {sock.family}' transport = "uds"
)
if transport is None:
raise NotImplementedError(f"Unsupported socket family: {fam}")
if not transport: if not transport:
raise NotImplementedError( raise NotImplementedError(
f'Could not figure out transport type for stream type {type(stream)}' f"Could not figure out transport type for stream type {type(stream)}"
) )
key = (codec_key, transport) key = (codec_key, transport)
return _key_to_transport[key] return _key_to_transport[key]