# to avoid hanging forever in self._wait as otherwise _exit_waiters
# would never be woken up, we wake them up here.
for waiter in self._exit_waiters:
- if not waiter.cancelled():
+ if not waiter.done():
waiter.set_result(self._returncode)
if all(p is not None and p.disconnected
for p in self._pipes.values()):
finally:
# wake up futures waiting for wait()
for waiter in self._exit_waiters:
- if not waiter.cancelled():
+ if not waiter.done():
waiter.set_result(self._returncode)
self._exit_waiters = None
self._loop = None
)
transport.close()
+ def test_proc_exited_no_invalid_state_error_on_exit_waiters(self):
+ # gh-145541: when _connect_pipes hasn't completed (so
+ # _pipes_connected is False) and the process exits, _try_finish()
+ # sets the result on exit waiters. Then _call_connection_lost() must
+ # not call set_result() again on the same waiters.
+ self.loop.set_exception_handler(
+ lambda loop, context: self.fail(
+ f"unexpected exception: {context}")
+ )
+ waiter = self.loop.create_future()
+ transport, protocol = self.create_transport(waiter)
+
+ # Simulate a waiter registered via _wait() before the process exits.
+ exit_waiter = self.loop.create_future()
+ transport._exit_waiters.append(exit_waiter)
+
+ # _connect_pipes hasn't completed, so _pipes_connected is False.
+ self.assertFalse(transport._pipes_connected)
+
+ # Simulate process exit. _try_finish() will set the result on
+ # exit_waiter because _pipes_connected is False, and then schedule
+ # _call_connection_lost() because _pipes is empty (vacuously all
+ # disconnected). _call_connection_lost() must skip exit_waiter
+ # because it's already done.
+ transport._process_exited(6)
+ self.loop.run_until_complete(waiter)
+
+ self.assertEqual(exit_waiter.result(), 6)
+
+ transport.close()
+
class SubprocessMixin: