Skip to content
This repository was archived by the owner on Oct 27, 2022. It is now read-only.

Commit 6ef1fea

Browse files
committed
* Fixes issue #21291: Popen.wait() is now thread safe so that multiple
threads may be calling wait() or poll() on a Popen instance at the same time without losing the Popen.returncode value. * Fixes issue #14396: Handle the odd rare case of waitpid returning 0 when not expected in Popen.wait().
1 parent 311b64c commit 6ef1fea

File tree

3 files changed

+118
-23
lines changed

3 files changed

+118
-23
lines changed

ChangeLog

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
-----------------
2-
2014-04-04 3.2.6
2+
2014-04-23 3.2.6
33
-----------------
44

5-
- Fixes issue #16962: Use getdents64 instead of the obsolete getdents syscall
5+
* Fixes issue #21291: Popen.wait() is now thread safe so that multiple
6+
threads may be calling wait() or poll() on a Popen instance at the same time
7+
without losing the Popen.returncode value.
8+
* Fixes issue #14396: Handle the odd rare case of waitpid returning 0 when not
9+
expected in Popen.wait().
10+
* Fixes issue #16962: Use getdents64 instead of the obsolete getdents syscall
611
on Linux. Some architectures do not implement the latter.
712

813
-----------------

subprocess32.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ class pywintypes:
470470
warnings.warn("The _posixsubprocess module is not being used. "
471471
"Child process reliability may suffer if your "
472472
"program uses threads.", RuntimeWarning)
473+
try:
474+
import threading
475+
except ImportError:
476+
import dummy_threading as threading
473477

474478
# When select or poll has indicated that the file is writable,
475479
# we can write up to _PIPE_BUF bytes without risk of blocking.
@@ -715,6 +719,12 @@ def __init__(self, args, bufsize=0, executable=None,
715719
pass_fds=()):
716720
"""Create new Popen instance."""
717721
_cleanup()
722+
# Held while anything is calling waitpid before returncode has been
723+
# updated to prevent clobbering returncode if wait() or poll() are
724+
# called from multiple threads at once. After acquiring the lock,
725+
# code must re-check self.returncode to see if another thread just
726+
# finished a waitpid() call.
727+
self._waitpid_lock = threading.Lock()
718728

719729
self._child_created = False
720730
self._input = None
@@ -1568,6 +1578,7 @@ def _dup2(a, b):
15681578
def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
15691579
_WTERMSIG=os.WTERMSIG, _WIFEXITED=os.WIFEXITED,
15701580
_WEXITSTATUS=os.WEXITSTATUS):
1581+
"""All callers to this function MUST hold self._waitpid_lock."""
15711582
# This method is called (indirectly) by __del__, so it cannot
15721583
# refer to anything outside of its local scope."""
15731584
if _WIFSIGNALED(sts):
@@ -1589,24 +1600,34 @@ def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
15891600
15901601
"""
15911602
if self.returncode is None:
1603+
if not self._waitpid_lock.acquire(False):
1604+
# Something else is busy calling waitpid. Don't allow two
1605+
# at once. We know nothing yet.
1606+
return None
15921607
try:
1593-
pid, sts = _waitpid(self.pid, _WNOHANG)
1594-
if pid == self.pid:
1595-
self._handle_exitstatus(sts)
1596-
except _os_error, e:
1597-
if _deadstate is not None:
1598-
self.returncode = _deadstate
1599-
elif e.errno == _ECHILD:
1600-
# This happens if SIGCLD is set to be ignored or
1601-
# waiting for child processes has otherwise been
1602-
# disabled for our process. This child is dead, we
1603-
# can't get the status.
1604-
# http://bugs.python.org/issue15756
1605-
self.returncode = 0
1608+
try:
1609+
if self.returncode is not None:
1610+
return self.returncode # Another thread waited.
1611+
pid, sts = _waitpid(self.pid, _WNOHANG)
1612+
if pid == self.pid:
1613+
self._handle_exitstatus(sts)
1614+
except _os_error, e:
1615+
if _deadstate is not None:
1616+
self.returncode = _deadstate
1617+
elif e.errno == _ECHILD:
1618+
# This happens if SIGCLD is set to be ignored or
1619+
# waiting for child processes has otherwise been
1620+
# disabled for our process. This child is dead, we
1621+
# can't get the status.
1622+
# http://bugs.python.org/issue15756
1623+
self.returncode = 0
1624+
finally:
1625+
self._waitpid_lock.release()
16061626
return self.returncode
16071627

16081628

16091629
def _try_wait(self, wait_flags):
1630+
"""All callers to this function MUST hold self._waitpid_lock."""
16101631
try:
16111632
(pid, sts) = _eintr_retry_call(os.waitpid, self.pid, wait_flags)
16121633
except OSError, e:
@@ -1639,19 +1660,36 @@ def wait(self, timeout=None, endtime=None):
16391660
# cribbed from Lib/threading.py in Thread.wait() at r71065.
16401661
delay = 0.0005 # 500 us -> initial delay of 1 ms
16411662
while True:
1642-
(pid, sts) = self._try_wait(os.WNOHANG)
1643-
assert pid == self.pid or pid == 0
1644-
if pid == self.pid:
1645-
self._handle_exitstatus(sts)
1646-
break
1663+
if self._waitpid_lock.acquire(False):
1664+
try:
1665+
if self.returncode is not None:
1666+
break # Another thread waited.
1667+
(pid, sts) = self._try_wait(os.WNOHANG)
1668+
assert pid == self.pid or pid == 0
1669+
if pid == self.pid:
1670+
self._handle_exitstatus(sts)
1671+
break
1672+
finally:
1673+
self._waitpid_lock.release()
16471674
remaining = self._remaining_time(endtime)
16481675
if remaining <= 0:
16491676
raise TimeoutExpired(self.args, timeout)
16501677
delay = min(delay * 2, remaining, .05)
16511678
time.sleep(delay)
1652-
elif self.returncode is None:
1653-
(pid, sts) = self._try_wait(0)
1654-
self._handle_exitstatus(sts)
1679+
else:
1680+
while self.returncode is None:
1681+
self._waitpid_lock.acquire()
1682+
try:
1683+
if self.returncode is not None:
1684+
break # Another thread waited.
1685+
(pid, sts) = self._try_wait(0)
1686+
# Check the pid and loop as waitpid has been known to
1687+
# return 0 even without WNOHANG in odd situations.
1688+
# http://bugs.python.org/issue14396.
1689+
if pid == self.pid:
1690+
self._handle_exitstatus(sts)
1691+
finally:
1692+
self._waitpid_lock.release()
16551693
return self.returncode
16561694

16571695

test_subprocess32.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import errno
99
import tempfile
1010
import time
11+
try:
12+
import threading
13+
except ImportError:
14+
threading = None
1115
import re
1216
#import sysconfig
1317
import select
@@ -868,6 +872,54 @@ def test_leaking_fds_on_error(self):
868872
if c.errno != 2: # ignore "no such file"
869873
raise
870874

875+
#@unittest.skipIf(threading is None, "threading required")
876+
def test_threadsafe_wait(self):
877+
"""Issue21291: Popen.wait() needs to be threadsafe for returncode."""
878+
proc = subprocess.Popen([sys.executable, '-c',
879+
'import time; time.sleep(12)'])
880+
self.assertEqual(proc.returncode, None)
881+
results = []
882+
883+
def kill_proc_timer_thread():
884+
results.append(('thread-start-poll-result', proc.poll()))
885+
# terminate it from the thread and wait for the result.
886+
proc.kill()
887+
proc.wait()
888+
results.append(('thread-after-kill-and-wait', proc.returncode))
889+
# this wait should be a no-op given the above.
890+
proc.wait()
891+
results.append(('thread-after-second-wait', proc.returncode))
892+
893+
# This is a timing sensitive test, the failure mode is
894+
# triggered when both the main thread and this thread are in
895+
# the wait() call at once. The delay here is to allow the
896+
# main thread to most likely be blocked in its wait() call.
897+
t = threading.Timer(0.2, kill_proc_timer_thread)
898+
t.start()
899+
900+
# Wait for the process to finish; the thread should kill it
901+
# long before it finishes on its own. Supplying a timeout
902+
# triggers a different code path for better coverage.
903+
proc.wait(timeout=20)
904+
# Should be -9 because of the proc.kill() from the thread.
905+
self.assertEqual(proc.returncode, -9,
906+
msg="unexpected result in wait from main thread")
907+
908+
# This should be a no-op with no change in returncode.
909+
proc.wait()
910+
self.assertEqual(proc.returncode, -9,
911+
msg="unexpected result in second main wait.")
912+
913+
t.join()
914+
# Ensure that all of the thread results are as expected.
915+
# When a race condition occurs in wait(), the returncode could
916+
# be set by the wrong thread that doesn't actually have it
917+
# leading to an incorrect value.
918+
self.assertEqual([('thread-start-poll-result', None),
919+
('thread-after-kill-and-wait', -9),
920+
('thread-after-second-wait', -9)],
921+
results)
922+
871923
def test_issue8780(self):
872924
# Ensure that stdout is inherited from the parent
873925
# if stdout=PIPE is not used

0 commit comments

Comments
 (0)