Skip to content

Commit e4e3395

Browse files
committed
Detach pipe handles from Popen after timeout to prevent __del__ race
After a subprocess timeout on Windows, the reader threads started by communicate() may still be draining buffered pipe output even after taskkill and proc.kill(). If they take longer than the 5-second join window, proc.__del__() runs while a thread is in fh.read() and closes the handle under it. That raises OSError in the thread, which triggers pytest's threading.excepthook and produces exit code 1 despite zero test failures. Setting proc.stdout = None and proc.stderr = None before returning prevents __del__() from seeing those handles. Each reader thread holds its own local reference to the file object (bound at Thread creation), reads until EOF, and calls fh.close() itself. No handle is leaked; the race is eliminated regardless of how long the drain takes.
1 parent 184e754 commit e4e3395

1 file changed

Lines changed: 12 additions & 6 deletions

File tree

meta_package_manager/base.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -856,17 +856,23 @@ def run(
856856
# that inherited the pipe write handles is still alive.
857857
# taskkill /F /T above closes those handles by killing the
858858
# entire tree, so the threads should exit via EOF within
859-
# milliseconds. Join them explicitly before returning so
860-
# that proc.__del__() does not race to close proc.stdout /
861-
# proc.stderr while a thread is still reading from the same
862-
# handle — that race raises OSError in the thread and
863-
# triggers pytest's threading.excepthook, causing exit
864-
# code 1 despite zero test failures.
859+
# milliseconds.
865860
proc.wait()
866861
for _attr in ("stdout_thread", "stderr_thread"):
867862
_t = getattr(proc, _attr, None)
868863
if _t is not None and _t.is_alive():
869864
_t.join(timeout=5)
865+
# Detach the pipe file objects from proc so that
866+
# proc.__del__() cannot close them while a reader thread
867+
# is still draining the pipe buffer. The thread holds its
868+
# own local reference (the fh argument passed at creation)
869+
# and calls fh.close() when it finishes, so no handle is
870+
# permanently leaked. Without this, __del__() racing with
871+
# a slow drain raises OSError in the thread, which triggers
872+
# pytest's threading.excepthook and produces exit code 1
873+
# despite zero test failures.
874+
proc.stdout = None
875+
proc.stderr = None
870876
stdout, stderr = b"", b""
871877
else:
872878
stdout, stderr = proc.communicate()

0 commit comments

Comments
 (0)