@@ -1423,6 +1423,8 @@ def test_wait(self):
14231423 def test_wait_timeout (self ):
14241424 p = subprocess .Popen ([sys .executable ,
14251425 "-c" , "import time; time.sleep(0.3)" ])
1426+ with self .assertRaises (subprocess .TimeoutExpired ) as c :
1427+ p .wait (timeout = 0 )
14261428 with self .assertRaises (subprocess .TimeoutExpired ) as c :
14271429 p .wait (timeout = 0.0001 )
14281430 self .assertIn ("0.0001" , str (c .exception )) # For coverage of __str__.
@@ -4094,5 +4096,122 @@ def test_broken_pipe_cleanup(self):
40944096 self .assertTrue (proc .stdin .closed )
40954097
40964098
4099+
4100+ class FastWaitTestCase (BaseTestCase ):
4101+ """Tests for efficient (pidfd_open() + poll() / kqueue()) process
4102+ waiting in subprocess.Popen.wait().
4103+ """
4104+ CAN_USE_PIDFD_OPEN = subprocess ._CAN_USE_PIDFD_OPEN
4105+ CAN_USE_KQUEUE = subprocess ._CAN_USE_KQUEUE
4106+ COMMAND = [sys .executable , "-c" , "import time; time.sleep(0.3)" ]
4107+ WAIT_TIMEOUT = 0.0001 # 0.1 ms
4108+
4109+ def assert_fast_waitpid_error (self , patch_point ):
4110+ # Emulate a case where pidfd_open() or kqueue() fails.
4111+ # Busy-poll wait should be used as fallback.
4112+ exc = OSError (errno .EMFILE , os .strerror (errno .EMFILE ))
4113+ with mock .patch (patch_point , side_effect = exc ) as m :
4114+ p = subprocess .Popen (self .COMMAND )
4115+ with self .assertRaises (subprocess .TimeoutExpired ):
4116+ p .wait (self .WAIT_TIMEOUT )
4117+ self .assertEqual (p .wait (timeout = support .SHORT_TIMEOUT ), 0 )
4118+ self .assertTrue (m .called )
4119+
4120+ @unittest .skipIf (not CAN_USE_PIDFD_OPEN , reason = "needs pidfd_open()" )
4121+ def test_wait_pidfd_open_error (self ):
4122+ self .assert_fast_waitpid_error ("os.pidfd_open" )
4123+
4124+ @unittest .skipIf (not CAN_USE_KQUEUE , reason = "needs kqueue() for proc" )
4125+ def test_wait_kqueue_error (self ):
4126+ self .assert_fast_waitpid_error ("select.kqueue" )
4127+
4128+ @unittest .skipIf (not CAN_USE_KQUEUE , reason = "needs kqueue() for proc" )
4129+ def test_kqueue_control_error (self ):
4130+ # Emulate a case where kqueue.control() fails. Busy-poll wait
4131+ # should be used as fallback.
4132+ p = subprocess .Popen (self .COMMAND )
4133+ kq_mock = mock .Mock ()
4134+ kq_mock .control .side_effect = OSError (
4135+ errno .EPERM , os .strerror (errno .EPERM )
4136+ )
4137+ kq_mock .close = mock .Mock ()
4138+
4139+ with mock .patch ("select.kqueue" , return_value = kq_mock ) as m :
4140+ with self .assertRaises (subprocess .TimeoutExpired ):
4141+ p .wait (self .WAIT_TIMEOUT )
4142+ self .assertEqual (p .wait (timeout = support .SHORT_TIMEOUT ), 0 )
4143+ self .assertTrue (m .called )
4144+
4145+ def assert_wait_race_condition (self , patch_target , real_func ):
4146+ # Call pidfd_open() / kqueue(), then terminate the process.
4147+ # Make sure that the wait call (poll() / kqueue.control())
4148+ # still works for a terminated PID.
4149+ p = subprocess .Popen (self .COMMAND )
4150+
4151+ def wrapper (* args , ** kwargs ):
4152+ ret = real_func (* args , ** kwargs )
4153+ try :
4154+ os .kill (p .pid , signal .SIGTERM )
4155+ os .waitpid (p .pid , 0 )
4156+ except OSError :
4157+ pass
4158+ return ret
4159+
4160+ with mock .patch (patch_target , side_effect = wrapper ) as m :
4161+ status = p .wait (timeout = support .SHORT_TIMEOUT )
4162+ self .assertTrue (m .called )
4163+ self .assertEqual (status , 0 )
4164+
4165+ @unittest .skipIf (not CAN_USE_PIDFD_OPEN , reason = "needs pidfd_open()" )
4166+ def test_pidfd_open_race (self ):
4167+ self .assert_wait_race_condition ("os.pidfd_open" , os .pidfd_open )
4168+
4169+ @unittest .skipIf (not CAN_USE_KQUEUE , reason = "needs kqueue() for proc" )
4170+ def test_kqueue_race (self ):
4171+ self .assert_wait_race_condition ("select.kqueue" , select .kqueue )
4172+
4173+ def assert_notification_without_immediate_reap (self , patch_target ):
4174+ # Verify fallback to busy polling when poll() / kqueue()
4175+ # succeeds, but waitpid(pid, WNOHANG) returns (0, 0).
4176+ def waitpid_wrapper (pid , flags ):
4177+ nonlocal ncalls
4178+ ncalls += 1
4179+ if ncalls == 1 :
4180+ return (0 , 0 )
4181+ return real_waitpid (pid , flags )
4182+
4183+ ncalls = 0
4184+ real_waitpid = os .waitpid
4185+ with mock .patch .object (subprocess .Popen , patch_target , return_value = True ) as m1 :
4186+ with mock .patch ("os.waitpid" , side_effect = waitpid_wrapper ) as m2 :
4187+ p = subprocess .Popen (self .COMMAND )
4188+ with self .assertRaises (subprocess .TimeoutExpired ):
4189+ p .wait (self .WAIT_TIMEOUT )
4190+ self .assertEqual (p .wait (timeout = support .SHORT_TIMEOUT ), 0 )
4191+ self .assertTrue (m1 .called )
4192+ self .assertTrue (m2 .called )
4193+
4194+ @unittest .skipIf (not CAN_USE_PIDFD_OPEN , reason = "needs pidfd_open()" )
4195+ def test_pidfd_open_notification_without_immediate_reap (self ):
4196+ self .assert_notification_without_immediate_reap ("_wait_pidfd" )
4197+
4198+ @unittest .skipIf (not CAN_USE_KQUEUE , reason = "needs kqueue() for proc" )
4199+ def test_kqueue_notification_without_immediate_reap (self ):
4200+ self .assert_notification_without_immediate_reap ("_wait_kqueue" )
4201+
4202+ @unittest .skipUnless (
4203+ CAN_USE_PIDFD_OPEN or CAN_USE_KQUEUE ,
4204+ "fast wait mechanism not available"
4205+ )
4206+ def test_fast_path_avoid_busy_loop (self ):
4207+ # assert that the busy loop is not called as long as the fast
4208+ # wait is available
4209+ with mock .patch ('time.sleep' ) as m :
4210+ p = subprocess .Popen (self .COMMAND )
4211+ with self .assertRaises (subprocess .TimeoutExpired ):
4212+ p .wait (self .WAIT_TIMEOUT )
4213+ self .assertEqual (p .wait (timeout = support .LONG_TIMEOUT ), 0 )
4214+ self .assertFalse (m .called )
4215+
40974216if __name__ == "__main__" :
40984217 unittest .main ()
0 commit comments