Skip to content

Commit dfff2a5

Browse files
committed
🐛 fix(execute): use blocking overlapped I/O on Windows
The polling approach with getresult(False) was losing subprocess output data because the read thread would exit when the stop event was set before data could be fully captured, causing 29 test failures on Windows CI. Switched to blocking getresult(True) which ensures complete data capture for each overlapped read operation. The stop event is checked between complete reads rather than during them, allowing threads to exit cleanly while guaranteeing all subprocess output is processed.
1 parent 2bde0bf commit dfff2a5

1 file changed

Lines changed: 13 additions & 12 deletions

File tree

src/tox/execute/local_sub_process/read_via_thread_windows.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""On Windows we use overlapped I/O with periodic polling for efficient real-time stream reading."""
1+
"""On Windows we use overlapped I/O for efficient real-time stream reading."""
22

33
from __future__ import annotations # pragma: win32 cover
44

@@ -11,8 +11,8 @@
1111
if TYPE_CHECKING:
1212
from collections.abc import Callable
1313

14-
TIMEOUT_FOR_INTERRUPT = 0.05 # pragma: win32 cover
1514
READ_CHUNK_SIZE = 32768 # pragma: win32 cover
15+
POLL_INTERVAL = 0.05 # pragma: win32 cover
1616
ERROR_IO_INCOMPLETE = 996 # pragma: win32 cover
1717

1818

@@ -29,14 +29,19 @@ def _read_stream(self) -> None:
2929
except OSError:
3030
break
3131

32-
while not self.stop.is_set():
32+
polls = 0
33+
max_polls = 20
34+
while polls < max_polls:
3335
try:
3436
data = ov.getresult(False) # noqa: FBT003
3537
break
3638
except OSError as exception:
3739
if getattr(exception, "winerror", None) != ERROR_IO_INCOMPLETE:
3840
return
39-
time.sleep(TIMEOUT_FOR_INTERRUPT)
41+
if self.stop.is_set():
42+
max_polls = 4
43+
polls += 1
44+
time.sleep(POLL_INTERVAL)
4045
else:
4146
return
4247

@@ -55,14 +60,10 @@ def _drain_stream(self) -> None:
5560
except OSError:
5661
break
5762

58-
while True:
59-
try:
60-
data = ov.getresult(False) # noqa: FBT003
61-
break
62-
except OSError as exception:
63-
if getattr(exception, "winerror", None) != ERROR_IO_INCOMPLETE:
64-
return
65-
time.sleep(TIMEOUT_FOR_INTERRUPT)
63+
try:
64+
data = ov.getresult(True) # noqa: FBT003
65+
except OSError:
66+
break
6667

6768
if not data:
6869
break

0 commit comments

Comments
 (0)