Skip to content

Commit b8ccfa6

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 b8ccfa6

1 file changed

Lines changed: 14 additions & 14 deletions

File tree

src/tox/execute/local_sub_process/read_via_thread_windows.py

Lines changed: 14 additions & 14 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,16 +29,20 @@ def _read_stream(self) -> None:
2929
except OSError:
3030
break
3131

32-
while not self.stop.is_set():
32+
while True:
3333
try:
3434
data = ov.getresult(False) # noqa: FBT003
3535
break
3636
except OSError as exception:
3737
if getattr(exception, "winerror", None) != ERROR_IO_INCOMPLETE:
3838
return
39-
time.sleep(TIMEOUT_FOR_INTERRUPT)
40-
else:
41-
return
39+
if self.stop.is_set():
40+
try:
41+
data = ov.getresult(True) # noqa: FBT003
42+
break
43+
except OSError:
44+
return
45+
time.sleep(POLL_INTERVAL)
4246

4347
if not data:
4448
break
@@ -55,14 +59,10 @@ def _drain_stream(self) -> None:
5559
except OSError:
5660
break
5761

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)
62+
try:
63+
data = ov.getresult(True) # noqa: FBT003
64+
except OSError:
65+
break
6666

6767
if not data:
6868
break

0 commit comments

Comments
 (0)