Commit 143b2d5f authored by Jason Madden's avatar Jason Madden Committed by GitHub

Fix #878 (#879)

* Fix #878

We can use multiple watchers to avoid any race conditions.

* Specific test cases for lines in os.waitpid.

* Less strict on the test since we may be running in parallel.

* make the leakchecker happy.
parent e4b21a1a
...@@ -159,6 +159,8 @@ Other Changes ...@@ -159,6 +159,8 @@ Other Changes
.. note:: Writing to the *io* property of a FileObject should be .. note:: Writing to the *io* property of a FileObject should be
considered deprecated after it is constructed. considered deprecated after it is constructed.
- The :func:`gevent.os.waitpid` function is cooperative in more
circumstances. Reported in :issue:`878` by Heungsub Lee.
1.1.2 (Jul 21, 2016) 1.1.2 (Jul 21, 2016)
==================== ====================
......
...@@ -260,30 +260,53 @@ if hasattr(os, 'fork'): ...@@ -260,30 +260,53 @@ if hasattr(os, 'fork'):
""" """
Wait for a child process to finish. Wait for a child process to finish.
If the child process was spawned using :func:`fork_and_watch`, then this If the child process was spawned using
function behaves cooperatively. If not, it *may* have race conditions; see :func:`fork_and_watch`, then this function behaves
cooperatively. If not, it *may* have race conditions; see
:func:`fork_gevent` for more information. :func:`fork_gevent` for more information.
The arguments are as for the underlying :func:`os.waitpid`. Some combinations The arguments are as for the underlying
of *options* may not be supported (as of 1.1 that includes WUNTRACED). :func:`os.waitpid`. Some combinations of *options* may not
be supported cooperatively (as of 1.1 that includes
WUNTRACED). Using a *pid* of 0 to request waiting on only processes
from the current process group is not cooperative.
Availability: POSIX. Availability: POSIX.
.. versionadded:: 1.1b1 .. versionadded:: 1.1b1
.. versionchanged:: 1.2a1
More cases are handled in a cooperative manner.
""" """
# XXX Does not handle tracing children # XXX Does not handle tracing children
# So long as libev's loop doesn't run, it's OK to add
# child watchers. The SIGCHLD handler only feeds events
# for the next iteration of the loop to handle. (And the
# signal handler itself is only called from the next loop
# iteration.)
if pid <= 0: if pid <= 0:
# magic functions for multiple children. # magic functions for multiple children.
if pid == -1: if pid == -1:
# Any child. If we have one that we're watching and that finished, # Any child. If we have one that we're watching and that finished,
# we need to use that one. Otherwise, let the OS take care of it. # we will use that one. Otherwise, let the OS take care of it.
for k, v in _watched_children.items(): for k, v in _watched_children.items():
if isinstance(v, tuple): if isinstance(v, tuple):
pid = k pid = k
break break
if pid <= 0: if pid <= 0:
# If we didn't find anything, go to the OS. Otherwise, # We didn't have one that was ready. If there are
# handle waiting # no funky options set, and the pid was -1
# (meaning any process, not 0, which means process
# group--- libev doesn't know about process
# groups) then we can use a child watcher of pid 0; otherwise,
# pass through to the OS.
if pid == -1 and options == 0:
hub = get_hub()
watcher = hub.loop.child(0, False)
hub.wait(watcher)
return watcher.rpid, watcher.rstatus
# There were funky options/pid, so we must go to the OS.
return _waitpid(pid, options) return _waitpid(pid, options)
if pid in _watched_children: if pid in _watched_children:
...@@ -300,17 +323,24 @@ if hasattr(os, 'fork'): ...@@ -300,17 +323,24 @@ if hasattr(os, 'fork'):
# it's not finished # it's not finished
return (0, 0) return (0, 0)
else: else:
# we should block. Let the underlying OS call block; it should # Ok, we need to "block". Do so via a watcher so that we're
# eventually die with OSError, depending on signal delivery # cooperative. We know it's our child, etc, so this should work.
try: watcher = _watched_children[pid]
return _waitpid(pid, options) # We can't start a watcher that's already started,
except OSError: # so we can't reuse the existing watcher.
if pid in _watched_children and isinstance(_watched_children, tuple): new_watcher = watcher.loop.child(pid, False)
result = _watched_children[pid] get_hub().wait(new_watcher)
del _watched_children[pid] # Ok, so now the new watcher is done. That means
return result[:2] # the old watcher's callback (_on_child) should
raise # have fired, potentially taking this child out of
# we're not watching it # _watched_children (but that could depend on how
# many callbacks there were to run, so use the
# watcher object directly; libev sets all the
# watchers at the same time).
return watcher.rpid, watcher.rstatus
# we're not watching it and it may not even be our child,
# so we must go to the OS to be sure to get the right semantics (exception)
return _waitpid(pid, options) return _waitpid(pid, options)
def fork_and_watch(callback=None, loop=None, ref=False, fork=fork_gevent): def fork_and_watch(callback=None, loop=None, ref=False, fork=fork_gevent):
......
import sys import sys
import _six as six import _six as six
from os import pipe from os import pipe
import gevent
from gevent import os from gevent import os
from greentest import TestCase, main from greentest import TestCase, main
from gevent import Greenlet, joinall from gevent import Greenlet, joinall
try:
import fcntl
except ImportError:
fcntl = None
try:
import errno
except ImportError:
errno = None
class TestOS_tp(TestCase): class TestOS_tp(TestCase):
...@@ -87,5 +79,29 @@ if hasattr(os, 'make_nonblocking'): ...@@ -87,5 +79,29 @@ if hasattr(os, 'make_nonblocking'):
return os.nb_write(*args) return os.nb_write(*args)
if hasattr(os, 'fork_and_watch'):
class TestForkAndWatch(TestCase):
__timeout__ = 5
def test_waitpid_all(self):
# Cover this specific case.
pid = os.fork_and_watch()
if pid:
os.waitpid(-1, 0)
# Can't assert on what the pid actually was,
# our testrunner may have spawned multiple children.
os._reap_children(0) # make the leakchecker happy
else:
gevent.sleep(2)
os._exit(0)
def test_waitpid_wrong_neg(self):
self.assertRaises(OSError, os.waitpid, -2, 0)
def test_waitpid_wrong_pos(self):
self.assertRaises(OSError, os.waitpid, 1, 0)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment