Fix Windows `import tractor` at the UDS root
The prior round gated UDS in four modules but `import tractor` still crashed on Windows: `tractor.ipc._uds` does `from socket import AF_UNIX` at module top, and several modules in the import graph (`discovery._api`, `spawn._reap`, `discovery._multiaddr`, `_testing.addr`) import `_uds` unconditionally. Instead of guarding every importer, fix the root and collapse the per-module probes to one capability flag. - in `ipc/_uds.py`, guard the lone `AF_UNIX` import so the module stays importable everywhere; expose `HAS_UDS = trio.has_unix` as the single source of truth (the same predicate that gates `trio.open_unix_socket()`). - `ipc/_types.py`, `discovery/_addr.py` and `ipc/_server.py` now import `UDSAddress`/`MsgpackUDSStream`/`HAS_UDS` directly and gate the transport + address registries on `HAS_UDS`; drop the duplicated `getattr(socket,'AF_UNIX')` / `platform.system()` probes, the dead `HAS_AF_UNIX` conjunct, and the import-time `log.warning()` spam. - `devx/_stackscope.py` `enable_stack_on_sig()` early-returns when `sig is None`, so a missing `SIGUSR1` (Windows) degrades to a no-op instead of a `TypeError` from `getsignal()` / `signal()`. - add a `windows-latest` CI leg (UDS excluded; informational via `continue-on-error` while support matures) plus an `import tractor` smoke step as the hard signal for the import fix. Because `_uds` is importable everywhere `UDSAddress` stays a real class, so `isinstance()` checks and `wrap_address()` no longer `AttributeError` on no-UDS hosts; actual socket use stays gated on `has_unix`. Review: https://github.com/goodboy/tractor/pull/475 (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
parent
5bf6417cc0
commit
d9532d1270
|
|
@ -91,6 +91,11 @@ jobs:
|
||||||
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
||||||
timeout-minutes: 16
|
timeout-minutes: 16
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -98,6 +103,7 @@ jobs:
|
||||||
os: [
|
os: [
|
||||||
ubuntu-latest,
|
ubuntu-latest,
|
||||||
macos-latest,
|
macos-latest,
|
||||||
|
windows-latest,
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
'3.13',
|
'3.13',
|
||||||
|
|
@ -123,6 +129,10 @@ jobs:
|
||||||
# don't do UDS run on macOS (for now)
|
# don't do UDS run on macOS (for now)
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
tpt_proto: 'uds'
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -150,6 +160,12 @@ jobs:
|
||||||
- name: List deps tree
|
- name: List deps tree
|
||||||
run: uv 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
|
- name: Run tests
|
||||||
run: >
|
run: >
|
||||||
uv run
|
uv run
|
||||||
|
|
|
||||||
|
|
@ -356,8 +356,8 @@ def dump_tree_on_sig(
|
||||||
|
|
||||||
|
|
||||||
def enable_stack_on_sig(
|
def enable_stack_on_sig(
|
||||||
sig: int = SIGUSR1,
|
sig: int|None = SIGUSR1,
|
||||||
) -> ModuleType:
|
) -> ModuleType|None:
|
||||||
'''
|
'''
|
||||||
Enable `stackscope` tracing on reception of a signal; by
|
Enable `stackscope` tracing on reception of a signal; by
|
||||||
default this is SIGUSR1.
|
default this is SIGUSR1.
|
||||||
|
|
@ -376,6 +376,16 @@ def enable_stack_on_sig(
|
||||||
>> pkill --signal SIGUSR1 -f <part-of-cmd: str>
|
>> pkill --signal SIGUSR1 -f <part-of-cmd: str>
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
# 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:
|
try:
|
||||||
# NOTE, `stackscope._glue` does intentional async-gen type
|
# NOTE, `stackscope._glue` does intentional async-gen type
|
||||||
# introspection at import-time which trips
|
# introspection at import-time which trips
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,6 @@ 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,
|
||||||
|
|
@ -36,27 +32,16 @@ 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,
|
||||||
|
HAS_UDS,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
|
|
||||||
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]:
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -190,11 +175,8 @@ class Address(Protocol):
|
||||||
_address_types: bidict[str, Type[Address]] = {
|
_address_types: bidict[str, Type[Address]] = {
|
||||||
'tcp': TCPAddress,
|
'tcp': TCPAddress,
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
if UDSAddress is not None:
|
|
||||||
_address_types['uds'] = UDSAddress
|
_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
|
||||||
|
|
@ -202,11 +184,8 @@ else:
|
||||||
_default_lo_addrs: dict[str, UnwrappedAddress] = {
|
_default_lo_addrs: dict[str, UnwrappedAddress] = {
|
||||||
'tcp': TCPAddress.get_root().unwrap(),
|
'tcp': TCPAddress.get_root().unwrap(),
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
if UDSAddress is not None:
|
|
||||||
_default_lo_addrs['uds'] = UDSAddress.get_root().unwrap()
|
_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]:
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
@ -72,16 +70,10 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
from ._tcp import TCPAddress
|
from ._tcp import TCPAddress
|
||||||
|
from ._uds import UDSAddress
|
||||||
|
|
||||||
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],
|
||||||
chan: Channel,
|
chan: Channel,
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,9 @@ IPC subsys type-lookup helpers?
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from typing import Type
|
from typing import Type
|
||||||
import platform
|
|
||||||
import socket
|
import socket
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from tractor.log import get_logger
|
|
||||||
from tractor.ipc._transport import (
|
from tractor.ipc._transport import (
|
||||||
MsgTransportKey,
|
MsgTransportKey,
|
||||||
MsgTransport,
|
MsgTransport,
|
||||||
|
|
@ -32,68 +30,37 @@ from tractor.ipc._tcp import (
|
||||||
TCPAddress,
|
TCPAddress,
|
||||||
MsgpackTCPStream,
|
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
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# manually updated list of all supported msg transport types
|
||||||
# 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
|
|
||||||
_msg_transports: list[Type[MsgTransport]] = [
|
_msg_transports: list[Type[MsgTransport]] = [
|
||||||
MsgpackTCPStream,
|
MsgpackTCPStream,
|
||||||
]
|
]
|
||||||
if HAS_UDS:
|
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]] = {
|
_key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = {
|
||||||
("msgpack", "tcp"): MsgpackTCPStream,
|
('msgpack', 'tcp'): MsgpackTCPStream,
|
||||||
}
|
}
|
||||||
if HAS_UDS:
|
if HAS_UDS:
|
||||||
_key_to_transport[("msgpack", "uds")] = MsgpackUDSStream # type: ignore
|
_key_to_transport[('msgpack', 'uds')] = MsgpackUDSStream
|
||||||
|
|
||||||
# Map Address wrapper -> transport type
|
# map an `Address`-wrapper to its `MsgTransport` type
|
||||||
_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { # type: ignore
|
_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = {
|
||||||
TCPAddress: MsgpackTCPStream,
|
TCPAddress: MsgpackTCPStream,
|
||||||
}
|
}
|
||||||
if HAS_UDS:
|
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)):
|
if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)):
|
||||||
transport = "tcp"
|
transport = "tcp"
|
||||||
|
|
||||||
# Only consider AF_UNIX when both Python exposes it and our backend is active
|
# only consider `AF_UNIX` when the UDS backend is active;
|
||||||
if transport is None and HAS_UDS and HAS_AF_UNIX and fam == socket.AF_UNIX: # type: ignore[attr-defined]
|
# `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"
|
transport = "uds"
|
||||||
|
|
||||||
if transport is None:
|
if transport is None:
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,21 @@ from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from socket import (
|
from socket import (
|
||||||
AF_UNIX,
|
|
||||||
SOCK_STREAM,
|
SOCK_STREAM,
|
||||||
SOL_SOCKET,
|
SOL_SOCKET,
|
||||||
error as socket_error,
|
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
|
import struct
|
||||||
from typing import (
|
from typing import (
|
||||||
Type,
|
Type,
|
||||||
|
|
@ -90,6 +100,13 @@ else:
|
||||||
log = get_logger()
|
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(
|
def unwrap_sockpath(
|
||||||
sockpath: Path,
|
sockpath: Path,
|
||||||
) -> tuple[Path, Path]:
|
) -> tuple[Path, Path]:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue