diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bb1297c..7697a4ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,11 @@ jobs: name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}' timeout-minutes: 16 runs-on: ${{ matrix.os }} + # Windows support is nascent: keep its legs informational so a + # known-incomplete area doesn't block merges. The `import + # tractor` smoke step below is the hard signal for this job; + # promote the whole leg to required once the suite is green. + continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false @@ -98,6 +103,7 @@ jobs: os: [ ubuntu-latest, macos-latest, + windows-latest, ] python-version: [ '3.13', @@ -123,6 +129,10 @@ jobs: # don't do UDS run on macOS (for now) - os: macos-latest tpt_proto: 'uds' + # UDS is POSIX-only; Windows has no `AF_UNIX` so the + # backend is intentionally unavailable there. + - os: windows-latest + tpt_proto: 'uds' steps: - uses: actions/checkout@v4 @@ -150,6 +160,12 @@ jobs: - name: List deps tree run: uv tree + # hard signal for the Windows import-safety fix: `import + # tractor` must succeed everywhere, and `HAS_UDS` reflects + # platform capability (False on Windows, True on POSIX). + - name: 'Smoke: import tractor' + run: uv run python -c "import tractor; from tractor.ipc._uds import HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" + - name: Run tests run: > uv run diff --git a/tractor/devx/_stackscope.py b/tractor/devx/_stackscope.py index bc791a4e..d0caafbd 100644 --- a/tractor/devx/_stackscope.py +++ b/tractor/devx/_stackscope.py @@ -356,8 +356,8 @@ def dump_tree_on_sig( def enable_stack_on_sig( - sig: int = SIGUSR1, -) -> ModuleType: + sig: int|None = SIGUSR1, +) -> ModuleType|None: ''' Enable `stackscope` tracing on reception of a signal; by default this is SIGUSR1. @@ -376,6 +376,16 @@ def enable_stack_on_sig( >> pkill --signal SIGUSR1 -f ''' + # no `SIGUSR1` on this platform (e.g. Windows) -> nothing to + # wire up; degrade gracefully instead of crashing callers that + # only guard against a missing `stackscope` (`ImportError`). + if sig is None: + log.warning( + 'No `SIGUSR1` on this platform;\n' + 'skipping `stackscope` trace-on-signal setup!\n' + ) + return None + try: # NOTE, `stackscope._glue` does intentional async-gen type # introspection at import-time which trips diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index ae6f957f..acdb6dad 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -22,10 +22,6 @@ from typing import ( TYPE_CHECKING, ) -import platform -import socket - - from bidict import bidict from trio import ( SocketListener, @@ -36,27 +32,16 @@ from ..runtime._state import ( _def_tpt_proto, ) from ..ipc._tcp import TCPAddress +from ..ipc._uds import ( + UDSAddress, + HAS_UDS, +) if TYPE_CHECKING: from ..runtime._runtime import Actor 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]: # ... @@ -190,11 +175,8 @@ class Address(Protocol): _address_types: bidict[str, Type[Address]] = { 'tcp': TCPAddress, } - -if UDSAddress is not None: +if HAS_UDS: _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 @@ -202,11 +184,8 @@ else: _default_lo_addrs: dict[str, UnwrappedAddress] = { 'tcp': TCPAddress.get_root().unwrap(), } - -if UDSAddress is not None: +if HAS_UDS: _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]: diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 4065b2cd..ace830bb 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -27,8 +27,6 @@ from functools import partial from itertools import chain import inspect import textwrap -import platform -import socket from types import ( ModuleType, ) @@ -72,16 +70,10 @@ if TYPE_CHECKING: from ._tcp import TCPAddress +from ._uds import UDSAddress 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], chan: Channel, diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index 33b35d9b..50505080 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -19,11 +19,9 @@ IPC subsys type-lookup helpers? ''' from typing import Type -import platform import socket import trio -from tractor.log import get_logger from tractor.ipc._transport import ( MsgTransportKey, MsgTransport, @@ -32,68 +30,37 @@ from tractor.ipc._tcp import ( TCPAddress, MsgpackTCPStream, ) +from tractor.ipc._uds import ( + UDSAddress, + MsgpackUDSStream, + HAS_UDS, +) -log = get_logger() +# the UDS backend is importable everywhere but only *usable* where +# `trio` reports `has_unix` (i.e. POSIX). On Windows / no-`AF_UNIX` +# hosts `HAS_UDS` is `False` and the runtime registers TCP only. +Address = TCPAddress|UDSAddress -# ------------------------------------------------------------ -# 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.") - -# ------------------------------------------------------------ -# 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 +# manually updated list of all supported msg transport types _msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, ] if HAS_UDS: - _msg_transports.append(MsgpackUDSStream) # type: ignore + _msg_transports.append(MsgpackUDSStream) -# Map MsgTransportKey -> transport type +# map a `MsgTransportKey` to its `MsgTransport` type _key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { - ("msgpack", "tcp"): MsgpackTCPStream, + ('msgpack', 'tcp'): MsgpackTCPStream, } if HAS_UDS: - _key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore + _key_to_transport[('msgpack', 'uds')] = MsgpackUDSStream -# Map Address wrapper -> transport type -_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore +# map an `Address`-wrapper to its `MsgTransport` type +_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { TCPAddress: MsgpackTCPStream, } if HAS_UDS: - _addr_to_transport[UDSAddress] = MsgpackUDSStream # type: ignore + _addr_to_transport[UDSAddress] = MsgpackUDSStream # ------------------------------------------------------------ @@ -132,8 +99,10 @@ def transport_from_stream( if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)): transport = "tcp" - # 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] + # only consider `AF_UNIX` when the UDS backend is active; + # `HAS_UDS` short-circuits before `socket.AF_UNIX` so this + # stays safe on hosts where that constant is absent. + if transport is None and HAS_UDS and fam == socket.AF_UNIX: # type: ignore[attr-defined] transport = "uds" if transport is None: diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 8c57664d..fcf831d7 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -25,11 +25,21 @@ from pathlib import Path import os import sys from socket import ( - AF_UNIX, SOCK_STREAM, SOL_SOCKET, error as socket_error, ) +# NOTE, `AF_UNIX` is absent on Windows / any CPython built without +# unix-domain-socket support. Keep this module importable +# everywhere (so `UDSAddress` stays referenceable for type and +# `isinstance()` checks plus registry lookups); the `AF_UNIX`-using +# code paths below are runtime-only and are never reached when the +# UDS backend is unusable (gated on `trio`'s `has_unix`, see +# `HAS_UDS`). +try: + from socket import AF_UNIX +except ImportError: + AF_UNIX = None import struct from typing import ( Type, @@ -90,6 +100,13 @@ else: log = get_logger() +# single source of truth for "is the UDS backend usable on this +# host?" — reuse `trio`'s `has_unix` (the same predicate that gates +# `trio.open_unix_socket()`) rather than re-deriving an `AF_UNIX` +# probe in every consumer module. +HAS_UDS: bool = has_unix + + def unwrap_sockpath( sockpath: Path, ) -> tuple[Path, Path]: