diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 529f8b11..d07ebf49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,10 @@ 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: its full test suite remains + # informational, while setup and the `import tractor` smoke below + # are hard signals. Promote the test step to required once the + # suite is green. strategy: fail-fast: false @@ -98,6 +102,7 @@ jobs: os: [ ubuntu-latest, macos-latest, + windows-latest, ] python-version: [ '3.13', @@ -118,6 +123,11 @@ jobs: 'tcp', 'uds', ] + exclude: + # 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 @@ -145,7 +155,14 @@ 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 sys; import tractor; from tractor.ipc._uds import HAS_UDS; assert sys.platform != 'win32' or not HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)" + - name: Run tests + continue-on-error: ${{ matrix.os == 'windows-latest' }} run: > uv run pytest diff --git a/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md new file mode 100644 index 00000000..81173fed --- /dev/null +++ b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md @@ -0,0 +1,39 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: pr475-review-fixes-20260817 +timestamp: 2026-08-17T23:18:25Z +git_ref: 359fe75c +scope: code +substantive: true +raw_file: 20260817T231825Z_359fe75c_prompt_io.raw.md +--- + +## Prompt + +Continue the `/code-review-changes` pass for PR #475 in its isolated +worktree. Address the seven accepted manual-review findings in +`tractor/ipc/_types.py` and `tractor/ipc/_uds.py`, preserve the existing +Windows capability behavior, verify the result, and prepare the work for +human-controlled commit and review-reply steps. Do not publish replies, +stage, commit, or push without the required explicit authorization. + +## Response summary + +Restored project quote, docstring, multiline-expression, and +`match/case` conventions while retaining the Windows-safe UDS guard. +Removed unnecessary structural and comment churn, then verified the +focused transport, discovery, and lazy-import paths plus the missing +`AF_UNIX` behavior. + +## Files changed + +- `tractor/ipc/_types.py` - restore project style and guarded + socket-family dispatch. +- `tractor/ipc/_uds.py` - format the UDS capability gate + consistently. + +## Human edits + +None - the generated patch remains uncommitted and awaits human +review. diff --git a/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md new file mode 100644 index 00000000..f9a2dcdd --- /dev/null +++ b/ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.raw.md @@ -0,0 +1,45 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-17T23:18:25Z +git_ref: 359fe75c +diff_cmd: git diff HEAD~1..HEAD +--- + +Applied the seven accepted manual-review fixes for PR #475 while +preserving the Windows transport capability behavior. + +> `git diff HEAD~1..HEAD -- tractor/ipc/_types.py` + +The generated changes restore the project's single-quote docstring and +string conventions, remove the unnecessary helper divider, simplify the +transport-registry comments, and restore `match/case` socket-family +dispatch. The UDS case retains a `HAS_UDS` guard that short-circuits +before `socket.AF_UNIX` is evaluated on unsupported hosts. Nearby error +messages are wrapped without changing their content. + +> `git diff HEAD~1..HEAD -- tractor/ipc/_uds.py` + +The generated change reformats the `HAS_UDS` conjunction according to +the project's multiline boolean-expression convention and simplifies +the adjacent capability comment. + +Verification: + +`/home/goodboy/repos/tractor/py313/bin/pytest -q tests/test_lazy_imports.py tests/discovery tests/ipc/test_server.py` + +Result: `66 passed, 2 xpassed in 60.62s`. + +`ruff check --no-cache --output-format=json tractor/ipc/_types.py tractor/ipc/_uds.py` + +Result: no findings. + +`git diff --check` + +Result: no whitespace errors. + +An explicit missing-`AF_UNIX` probe set `HAS_UDS = False`, removed the +socket constant, and exercised an unsupported socket family. It raised +the expected `NotImplementedError` instead of `AttributeError`. + +No review replies, commits, or pushes were published. diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index bdb1b852..4787fbbb 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -20,6 +20,18 @@ from typing import ( import pytest import trio import tractor + +# `infect_asyncio` mode is unsupported on Windows (asyncio's +# `ProactorEventLoop` is incompatible with our `trio` guest-mode +# interop and currently hangs/crashes the run). Skip the module on +# Windows so the CI leg completes + reports the rest of the suite. +import platform +if platform.system() == 'Windows': + pytest.skip( + 'infect_asyncio mode is unsupported on Windows', + allow_module_level=True, + ) + from tractor import ( current_actor, Actor, diff --git a/tests/test_ringbuf.py b/tests/test_ringbuf.py index e55a87b9..2c739568 100644 --- a/tests/test_ringbuf.py +++ b/tests/test_ringbuf.py @@ -1,10 +1,22 @@ import time +import platform import trio import pytest import tractor +# `tractor.ipc._ringbuf` is built on linux `eventfd(2)`; importing +# it pulls in `tractor.ipc._linux` whose module-level +# `ffi.dlopen(None)` raises on non-linux. Skip the whole module at +# COLLECTION before that crashing import runs (a `pytestmark` skip +# is too late — markers apply only after the import succeeds). +if platform.system() != 'Linux': + pytest.skip( + 'ringbuf (eventfd) IPC is linux-only', + allow_module_level=True, + ) + # XXX `cffi` dun build on py3.14 yet.. pytest.importorskip("cffi") diff --git a/tests/test_root_infect_asyncio.py b/tests/test_root_infect_asyncio.py index e7a307bc..cde5577f 100644 --- a/tests/test_root_infect_asyncio.py +++ b/tests/test_root_infect_asyncio.py @@ -9,6 +9,17 @@ from functools import partial import pytest import trio import tractor + +# `infect_asyncio` mode is unsupported on Windows (see +# `test_infected_asyncio`); skip at COLLECTION before the +# asyncio-interop imports below so the CI leg completes. +import platform +if platform.system() == 'Windows': + pytest.skip( + 'infect_asyncio mode is unsupported on Windows', + allow_module_level=True, + ) + from tractor import ( to_asyncio, ) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 782af81e..8a73d6aa 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -73,6 +73,11 @@ async def test_lifetime_stack_wipes_tmpfile( 1.6 if error_in_child else 1 ) + # scale for slow/noisy CI (esp. macOS) so the child error + # propagates before the deadline; otherwise `move_on_after` + # cancels first and flips the `error_in_child=True` assert. + from .conftest import cpu_perf_headroom + timeout *= cpu_perf_headroom() try: with trio.move_on_after(timeout) as cs: async with tractor.open_nursery( diff --git a/tests/test_shm.py b/tests/test_shm.py index 84d0988e..e63aef07 100644 --- a/tests/test_shm.py +++ b/tests/test_shm.py @@ -120,6 +120,13 @@ async def child_read_shm_list( print(f'(child): reading frame: {frame}') +@pytest.mark.skipif( + platform.system() == 'Windows', + reason=( + 'parent/child shm IPC deadlocks on Windows ' + '(frame-size dependent hang); nascent — see #404' + ), +) @pytest.mark.parametrize( 'use_str', [False, True], diff --git a/tractor/devx/_stackscope.py b/tractor/devx/_stackscope.py index e6ea00e5..d0caafbd 100644 --- a/tractor/devx/_stackscope.py +++ b/tractor/devx/_stackscope.py @@ -31,12 +31,21 @@ from threading import ( RLock, ) import multiprocessing as mp + +import platform + 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 ( @@ -347,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. @@ -367,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 8d440bb3..cf2f8a85 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -32,7 +32,10 @@ from ..runtime._state import ( _def_tpt_proto, ) from ..ipc._tcp import TCPAddress -from ..ipc._uds import UDSAddress +from ..ipc._uds import ( + UDSAddress, + HAS_UDS, +) if TYPE_CHECKING: # ONLY type-annots, the eager import costs ~4.5ms @@ -43,7 +46,6 @@ else: log = get_logger() - # TODO, maybe breakout the netns key to a struct? # class NetNs(Struct)[str, int]: # ... @@ -174,25 +176,36 @@ class Address(Protocol): ... +# the address types available on this host: TCP always, UDS only +# where usable (`HAS_UDS`). Both registries derive from this single +# list via each type's `proto_key`. +_address_protos: list[Type[Address]] = [TCPAddress] +if HAS_UDS: + _address_protos.append(UDSAddress) + _address_types: dict[str, Type[Address]] = { - 'tcp': TCPAddress, - 'uds': UDSAddress + cls.proto_key: cls + for cls in _address_protos } # 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 -] = { - 'tcp': TCPAddress.get_root().unwrap(), - 'uds': UDSAddress.get_root().unwrap(), +_default_lo_addrs: dict[str, UnwrappedAddress] = { + cls.proto_key: cls.get_root().unwrap() + for cls in _address_protos } def get_address_cls(name: str) -> Type[Address]: - return _address_types[name] + try: + return _address_types[name] + except KeyError: + raise NotImplementedError( + f'No IPC transport backend for {name!r} on this ' + f'platform!\n' + f'(available: {list(_address_types)})\n' + ) def is_wrapped_addr(addr: any) -> bool: @@ -290,7 +303,14 @@ def default_lo_addrs( for an input transport key set. ''' - return [ - _default_lo_addrs[transport] - for transport in transports - ] + lo_addrs: list[UnwrappedAddress] = [] + for transport in transports: + try: + lo_addrs.append(_default_lo_addrs[transport]) + except KeyError: + raise NotImplementedError( + f'No default loopback addr for transport ' + f'{transport!r} on this platform!\n' + f'(available: {list(_default_lo_addrs)})\n' + ) + return lo_addrs diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 80f85841..1775bd92 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -62,19 +62,20 @@ 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 +from ._uds import UDSAddress + log = log.get_logger() _PRE_REG_HANDSHAKE_TIMEOUT: float = 10 - 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 59653b17..244da48b 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -18,17 +18,13 @@ IPC subsys type-lookup helpers? ''' -from typing import ( - Type, - # TYPE_CHECKING, -) - -import trio +from typing import Type import socket +import trio from tractor.ipc._transport import ( MsgTransportKey, - MsgTransport + MsgTransport, ) from tractor.ipc._tcp import ( TCPAddress, @@ -37,37 +33,33 @@ from tractor.ipc._tcp import ( from tractor.ipc._uds import ( UDSAddress, MsgpackUDSStream, + HAS_UDS, ) -# if TYPE_CHECKING: -# from tractor._addr import Address - - +# the UDS backend is importable everywhere but only *usable* when +# `HAS_UDS` is `True`; otherwise the runtime registers TCP only. Address = TCPAddress|UDSAddress -# manually updated list of all supported msg transport types -_msg_transports = [ +# the available msg-transport backends on this host: TCP always, +# UDS only where usable (`HAS_UDS`). The lookup maps below derive +# from this single list via each backend's `codec_key` and +# `address_type`: register a backend here and every map picks it up. +_msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, - MsgpackUDSStream ] +if HAS_UDS: + _msg_transports.append(MsgpackUDSStream) - -# convert a MsgTransportKey to the corresponding transport type -_key_to_transport: dict[ - MsgTransportKey, - Type[MsgTransport], -] = { - ('msgpack', 'tcp'): MsgpackTCPStream, - ('msgpack', 'uds'): MsgpackUDSStream, +# map a `MsgTransportKey` -> `MsgTransport` type +_key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { + (t.codec_key, t.address_type.proto_key): t + for t in _msg_transports } -# convert an Address wrapper to its corresponding transport type -_addr_to_transport: dict[ - Type[TCPAddress|UDSAddress], - Type[MsgTransport] -] = { - TCPAddress: MsgpackTCPStream, - UDSAddress: MsgpackUDSStream, +# map an `Address`-wrapper -> `MsgTransport` type +_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = { + t.address_type: t + for t in _msg_transports } @@ -81,41 +73,51 @@ def transport_from_addr( ''' try: - return _addr_to_transport[type(addr)] + addr_type = type(addr) + return _addr_to_transport[addr_type] except KeyError: raise NotImplementedError( - f'No known transport for address {repr(addr)}' + f'No known transport for address ' + f'{addr!r}' ) 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 + transport: str|None = None + if isinstance(stream, trio.SocketStream): sock: socket.socket = stream.socket match sock.family: case socket.AF_INET | socket.AF_INET6: transport = 'tcp' - case socket.AF_UNIX: + # `HAS_UDS` short-circuits before `socket.AF_UNIX` on + # hosts where that constant is absent. + case fam if ( + HAS_UDS + and + fam == socket.AF_UNIX + ): transport = 'uds' - case _: + case fam: raise NotImplementedError( - f'Unsupported socket family: {sock.family}' + 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 ' + f'{type(stream)}' ) key = (codec_key, transport) diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 1986ce57..49a9322d 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -26,11 +26,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 ( Any, @@ -104,6 +114,16 @@ _SUN_PATH_LIMIT: int = ( ) +# single source of truth for whether the UDS backend is usable on this +# host. Windows can expose `AF_UNIX`, but this backend remains +# POSIX-only until its credential and lifecycle paths are supported. +HAS_UDS: bool = ( + sys.platform != 'win32' + and + has_unix +) + + def unwrap_sockpath( sockpath: Path, ) -> tuple[Path, Path]: