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