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 9eaaf2bd75
4 changed files with 131 additions and 64 deletions

View File

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

View File

@ -22,6 +22,10 @@ from typing import (
TYPE_CHECKING,
)
import platform
import socket
from bidict import bidict
from trio import (
SocketListener,
@ -32,7 +36,6 @@ from ..runtime._state import (
_def_tpt_proto,
)
from ..ipc._tcp import TCPAddress
from ..ipc._uds import UDSAddress
if TYPE_CHECKING:
from ..runtime._runtime import Actor
@ -40,6 +43,20 @@ if TYPE_CHECKING:
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?
# class NetNs(Struct)[str, int]:
# ...
@ -172,20 +189,25 @@ class Address(Protocol):
_address_types: bidict[str, Type[Address]] = {
'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
# when none is provided to a root actor on first boot.
_default_lo_addrs: dict[
str,
UnwrappedAddress
] = {
_default_lo_addrs: dict[str, UnwrappedAddress] = {
'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]:
return _address_types[name]

View File

@ -27,6 +27,8 @@ from functools import partial
from itertools import chain
import inspect
import textwrap
import platform
import socket
from types import (
ModuleType,
)
@ -62,16 +64,23 @@ from .. import log
from ..discovery._addr import Address
from ._chan import Channel
from ._transport import MsgTransport
from ._uds import UDSAddress
from ._tcp import TCPAddress
if TYPE_CHECKING:
from ..runtime._runtime import Actor
from ..runtime._supervise import ActorNursery
from ._tcp import TCPAddress
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(
uid: tuple[str, str],

View File

@ -18,106 +18,132 @@
IPC subsys type-lookup helpers?
'''
from typing import (
Type,
# TYPE_CHECKING,
)
import trio
from typing import Type
import logging
import platform
import socket
import trio
from tractor.ipc._transport import (
MsgTransportKey,
MsgTransport
MsgTransport,
)
from tractor.ipc._tcp import (
TCPAddress,
MsgpackTCPStream,
)
from tractor.ipc._uds import (
UDSAddress,
MsgpackUDSStream,
log = logging.getLogger(__name__)
# ------------------------------------------------------------
# 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"
HAS_UDS = False
UDSAddress = None # type: ignore
MsgpackUDSStream = None # type: ignore
if HAS_AF_UNIX and not IS_WINDOWS:
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.")
# if TYPE_CHECKING:
# from tractor._addr import Address
# ------------------------------------------------------------
# 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
Address = TCPAddress|UDSAddress
# manually updated list of all supported msg transport types
_msg_transports = [
# Manually updated list of all supported msg transport types
_msg_transports: list[Type[MsgTransport]] = [
MsgpackTCPStream,
MsgpackUDSStream
]
if HAS_UDS:
_msg_transports.append(MsgpackUDSStream) # type: ignore
# convert a MsgTransportKey to the corresponding transport type
_key_to_transport: dict[
MsgTransportKey,
Type[MsgTransport],
] = {
('msgpack', 'tcp'): MsgpackTCPStream,
('msgpack', 'uds'): MsgpackUDSStream,
# Map MsgTransportKey -> transport type
_key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = {
("msgpack", "tcp"): MsgpackTCPStream,
}
if HAS_UDS:
_key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore
# convert an Address wrapper to its corresponding transport type
_addr_to_transport: dict[
Type[TCPAddress|UDSAddress],
Type[MsgTransport]
] = {
# Map Address wrapper -> transport type
_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore
TCPAddress: MsgpackTCPStream,
UDSAddress: MsgpackUDSStream,
}
if HAS_UDS:
_addr_to_transport[UDSAddress] = MsgpackUDSStream # type: ignore
# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
def transport_from_addr(
addr: Address,
codec_key: str = 'msgpack',
codec_key: str = "msgpack",
) -> Type[MsgTransport]:
'''
"""
Given a destination address and a desired codec, find the
corresponding `MsgTransport` type.
'''
"""
try:
return _addr_to_transport[type(addr)]
return _addr_to_transport[type(addr)] # type: ignore[call-arg]
except KeyError:
raise NotImplementedError(
f'No known transport for address {repr(addr)}'
f"No known transport for address {repr(addr)}"
)
def transport_from_stream(
stream: trio.abc.Stream,
codec_key: str = 'msgpack'
codec_key: str = "msgpack",
) -> Type[MsgTransport]:
'''
"""
Given an arbitrary `trio.abc.Stream` and a desired codec,
find the corresponding `MsgTransport` type.
'''
"""
transport = None
if isinstance(stream, trio.SocketStream):
sock: socket.socket = stream.socket
match sock.family:
case socket.AF_INET | socket.AF_INET6:
transport = 'tcp'
fam = sock.family
case socket.AF_UNIX:
transport = 'uds'
if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)):
transport = "tcp"
case _:
raise NotImplementedError(
f'Unsupported socket family: {sock.family}'
)
# Only consider AF_UNIX when both Python exposes it and our backend is active
if transport is None and HAS_UDS and HAS_AF_UNIX and fam == socket.AF_UNIX: # type: ignore[attr-defined]
transport = "uds"
if transport is None:
raise NotImplementedError(f"Unsupported socket family: {fam}")
if not transport:
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)
return _key_to_transport[key]