@@ -112,37 +112,6 @@ def test_subprocess_repr(self):
112112 )
113113 transport .close ()
114114
115- def test_proc_exited_no_invalid_state_error_on_exit_waiters (self ):
116- # gh-145541: when _connect_pipes hasn't completed (so
117- # _pipes_connected is False) and the process exits, _try_finish()
118- # sets the result on exit waiters. Then _call_connection_lost() must
119- # not call set_result() again on the same waiters.
120- self .loop .set_exception_handler (
121- lambda loop , context : self .fail (
122- f"unexpected exception: { context } " )
123- )
124- waiter = self .loop .create_future ()
125- transport , protocol = self .create_transport (waiter )
126-
127- # Simulate a waiter registered via _wait() before the process exits.
128- exit_waiter = self .loop .create_future ()
129- transport ._exit_waiters .append (exit_waiter )
130-
131- # _connect_pipes hasn't completed, so _pipes_connected is False.
132- self .assertFalse (transport ._pipes_connected )
133-
134- # Simulate process exit. _try_finish() will set the result on
135- # exit_waiter because _pipes_connected is False, and then schedule
136- # _call_connection_lost() because _pipes is empty (vacuously all
137- # disconnected). _call_connection_lost() must skip exit_waiter
138- # because it's already done.
139- transport ._process_exited (6 )
140- self .loop .run_until_complete (waiter )
141-
142- self .assertEqual (exit_waiter .result (), 6 )
143-
144- transport .close ()
145-
146115
147116class SubprocessMixin :
148117
@@ -436,6 +405,46 @@ async def len_message(message):
436405 self .assertEqual (output .rstrip (), b'3' )
437406 self .assertEqual (exitcode , 0 )
438407
408+ def test_wait_even_if_pipe_is_open (self ):
409+ # gh-119710: Process.wait() must return once the process exits even
410+ # if its stdout pipe is inherited by a grandchild that keeps it open,
411+ # so the pipe never reaches EOF. Otherwise wait() hangs forever
412+ # despite the returncode being known.
413+
414+ async def run ():
415+ # The grandchild inherits the child's stdin and stdout pipes and
416+ # keeps both open after the child is killed. It writes "ready"
417+ # so we know it has started, and exits once its stdin hits EOF.
418+ code = textwrap .dedent ("""\
419+ import subprocess, sys
420+ subprocess.run([sys.executable, "-c",
421+ "import sys; sys.stdout.write('ready');"
422+ " sys.stdout.flush(); sys.stdin.read()"])
423+ """ )
424+
425+ proc = await asyncio .create_subprocess_exec (
426+ sys .executable , "-c" , code ,
427+ stdin = subprocess .PIPE ,
428+ stdout = subprocess .PIPE ,
429+ )
430+ try :
431+ wait_proc = asyncio .create_task (proc .wait ())
432+ # Wait until the grandchild holds the inherited pipes; this
433+ # also lets the wait() task register its waiter.
434+ await proc .stdout .readexactly (5 )
435+ proc .kill ()
436+ returncode = await asyncio .wait_for (
437+ wait_proc , timeout = support .SHORT_TIMEOUT )
438+ if sys .platform == 'win32' :
439+ self .assertIsInstance (returncode , int )
440+ else :
441+ self .assertEqual (- signal .SIGKILL , returncode )
442+ finally :
443+ proc .stdin .close () # let the grandchild exit
444+ await proc .stdout .read ()
445+
446+ self .loop .run_until_complete (run ())
447+
439448 def test_empty_input (self ):
440449
441450 async def empty_input ():
0 commit comments