Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/3551.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid importing ``asyncio`` on Windows by implementing ``windows_pipe`` directly with ``_winapi``, reducing startup latency.
10 changes: 2 additions & 8 deletions src/trio/_subprocess_platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def create_pipe_from_child_output() -> tuple[ClosableReceiveStream, int]:
except ImportError as ex: # pragma: no cover
_wait_child_exiting_error = ex


try:
if TYPE_CHECKING:
# Not worth type checking these definitions
Expand All @@ -96,15 +97,8 @@ def create_pipe_from_child_output() -> tuple[trio.lowlevel.FdStream, int]:
elif os.name == "nt":
import msvcrt

# This isn't exported or documented, but it's also not
# underscore-prefixed, and seems kosher to use. The asyncio docs
# for 3.5 included an example that imported socketpair from
# windows_utils (before socket.socketpair existed on Windows), and
# when asyncio.windows_utils.socketpair was removed in 3.7, the
# removal was mentioned in the release notes.
from asyncio.windows_utils import pipe as windows_pipe

from .._windows_pipes import PipeReceiveStream, PipeSendStream
from .windows import windows_pipe

def create_pipe_to_child_stdin() -> tuple[PipeSendStream, int]:
# for stdin, we want the write end (our end) to use overlapped I/O
Expand Down
82 changes: 81 additions & 1 deletion src/trio/_subprocess_platform/windows.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,91 @@
from __future__ import annotations

import _winapi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's... not rely on _winapi :-)

import itertools
import os
import tempfile
from typing import TYPE_CHECKING

from .._wait_for_object import WaitForSingleObject

if TYPE_CHECKING:
from .. import _subprocess

_pipe_counter = itertools.count()


def windows_pipe(
*,
duplex: bool = False,
overlapped: tuple[bool, bool] = (True, True),
bufsize: int = 8192,
) -> tuple[int, int]:
"""Like os.pipe() but with overlapped support and using handles not fds.

Adapted from CPython's asyncio.windows_utils.pipe().
Copyright (c) 2001-2026 Python Software Foundation; All Rights Reserved.
Licensed under the Python Software Foundation License Version 2.

Changes from CPython:
- Renamed pipe prefix from r'\\.\\pipe\asyncio-pipe-*' to r'\\.\\pipe\trio-pipe-*'.
- Modernized type hints for Trio's static typing and mypy checks.
- Explicit exception guard ensures early handle is closed if connection fails.
"""
address = tempfile.mktemp(
prefix=rf"\\.\pipe\trio-pipe-{os.getpid():d}-{next(_pipe_counter):d}-"
)

if duplex:
openmode = _winapi.PIPE_ACCESS_DUPLEX
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
obsize, ibsize = bufsize, bufsize
else:
openmode = _winapi.PIPE_ACCESS_INBOUND
access = _winapi.GENERIC_WRITE
obsize, ibsize = 0, bufsize

openmode |= _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE

if overlapped[0]:
openmode |= _winapi.FILE_FLAG_OVERLAPPED

flags_and_attribs = _winapi.FILE_FLAG_OVERLAPPED if overlapped[1] else 0

h1: int | None = None
h2: int | None = None
try:
h1 = _winapi.CreateNamedPipe(
address,
openmode,
_winapi.PIPE_WAIT,
1,
obsize,
ibsize,
_winapi.NMPWAIT_WAIT_FOREVER,
_winapi.NULL,
)

h2 = _winapi.CreateFile(
address,
access,
0,
_winapi.NULL,
_winapi.OPEN_EXISTING,
flags_and_attribs,
_winapi.NULL,
)

ov = _winapi.ConnectNamedPipe(h1, overlapped=True)
ov.GetOverlappedResult(True)
return h1, h2
except:
if h1 is not None:
_winapi.CloseHandle(h1)
if h2 is not None:
_winapi.CloseHandle(h2)
raise


async def wait_child_exiting(process: "_subprocess.Process") -> None:
async def wait_child_exiting(process: _subprocess.Process) -> None:
# _handle is not in Popen stubs, though it is present on Windows.
await WaitForSingleObject(int(process._proc._handle)) # type: ignore[attr-defined]
33 changes: 25 additions & 8 deletions src/trio/_tests/test_trio.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
def test_trio_import() -> None:
import sys

for module in list(sys.modules.keys()):
if module.startswith("trio"):
del sys.modules[module]

import trio # noqa: F401
from __future__ import annotations

import subprocess
import sys


def test_trio_import() -> None:
for module in list(sys.modules.keys()):
if module.startswith("trio"):
del sys.modules[module]

import trio # noqa: F401


def test_trio_import_does_not_import_asyncio() -> None:
res = subprocess.run(
[
sys.executable,
"-c",
"import sys, trio; assert 'asyncio' not in sys.modules, f'unexpected asyncio modules: {[m for m in sys.modules if \"asyncio\" in m]}'",
],
capture_output=True,
text=True,
)
assert res.returncode == 0, f"import trio loaded asyncio:\n{res.stderr}"