@@ -111,6 +111,31 @@ 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+ waiter = self .loop .create_future ()
120+ transport , protocol = self .create_transport (waiter )
121+
122+ # Simulate a waiter registered via _wait() before the process exits.
123+ exit_waiter = self .loop .create_future ()
124+ transport ._exit_waiters .append (exit_waiter )
125+
126+ # _connect_pipes hasn't completed, so _pipes_connected is False.
127+ self .assertFalse (transport ._pipes_connected )
128+
129+ # Simulate process exit. _try_finish() will set the result on
130+ # exit_waiter because _pipes_connected is False, and then schedule
131+ # _call_connection_lost() because _pipes is empty (vacuously all
132+ # disconnected). _call_connection_lost() must skip exit_waiter
133+ # because it's already done.
134+ transport ._process_exited (6 )
135+ self .loop .run_until_complete (waiter )
136+
137+ self .assertEqual (exit_waiter .result (), 6 )
138+
114139
115140class SubprocessMixin :
116141
@@ -918,6 +943,36 @@ async def main():
918943 asyncio .run (main ())
919944 gc_collect ()
920945
946+ @warnings_helper .ignore_warnings (category = ResourceWarning )
947+ def test_subprocess_pipe_cancelled_no_invalid_state_error (self ):
948+ # gh-145541: when _connect_pipes is cancelled and the process
949+ # subsequently exits, _call_connection_lost() must not raise
950+ # InvalidStateError by calling set_result() on exit waiters that
951+ # were already resolved by _try_finish().
952+ async def main ():
953+ loop = asyncio .get_running_loop ()
954+ loop .connect_read_pipe = mock .AsyncMock (
955+ side_effect = asyncio .CancelledError ,
956+ )
957+
958+ proc = None
959+ with self .assertRaises (asyncio .CancelledError ):
960+ proc = await asyncio .create_subprocess_exec (
961+ * PROGRAM_BLOCKED ,
962+ stdout = asyncio .subprocess .PIPE ,
963+ )
964+
965+ if proc is not None :
966+ proc .kill ()
967+ # wait() adds an exit waiter. Before the fix,
968+ # _call_connection_lost() would call set_result() on it
969+ # after _try_finish() already did, raising
970+ # InvalidStateError.
971+ await proc .wait ()
972+
973+ asyncio .run (main ())
974+ gc_collect ()
975+
921976if sys .platform != 'win32' :
922977 # Unix
923978 class SubprocessWatcherMixin (SubprocessMixin ):
0 commit comments