Skip to content

Commit cd53b7f

Browse files
authored
Prohibit adding a signal handler for SIGCHLD (#156)
1 parent 82013cf commit cd53b7f

File tree

6 files changed

+130
-70
lines changed

6 files changed

+130
-70
lines changed

README.rst

-8
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,6 @@ loop policy:
5252
import uvloop
5353
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
5454
55-
Alternatively, you can create an instance of the loop
56-
manually, using:
57-
58-
.. code:: python
59-
60-
loop = uvloop.new_event_loop()
61-
asyncio.set_event_loop(loop)
62-
6355
6456
Development of uvloop
6557
---------------------

tests/test_base.py

+54-52
Original file line numberDiff line numberDiff line change
@@ -461,56 +461,6 @@ def handler(loop, context):
461461
self.mock_pattern('Unhandled error in exception handler'),
462462
exc_info=mock.ANY)
463463

464-
def test_default_exc_handler_broken(self):
465-
logger = logging.getLogger('asyncio')
466-
_context = None
467-
468-
class Loop(uvloop.Loop):
469-
470-
_selector = mock.Mock()
471-
_process_events = mock.Mock()
472-
473-
def default_exception_handler(self, context):
474-
nonlocal _context
475-
_context = context
476-
# Simulates custom buggy "default_exception_handler"
477-
raise ValueError('spam')
478-
479-
loop = Loop()
480-
self.addCleanup(loop.close)
481-
asyncio.set_event_loop(loop)
482-
483-
def run_loop():
484-
def zero_error():
485-
loop.stop()
486-
1 / 0
487-
loop.call_soon(zero_error)
488-
loop.run_forever()
489-
490-
with mock.patch.object(logger, 'error') as log:
491-
run_loop()
492-
log.assert_called_with(
493-
'Exception in default exception handler',
494-
exc_info=True)
495-
496-
def custom_handler(loop, context):
497-
raise ValueError('ham')
498-
499-
_context = None
500-
loop.set_exception_handler(custom_handler)
501-
with mock.patch.object(logger, 'error') as log:
502-
run_loop()
503-
log.assert_called_with(
504-
self.mock_pattern('Exception in default exception.*'
505-
'while handling.*in custom'),
506-
exc_info=True)
507-
508-
# Check that original context was passed to default
509-
# exception handler.
510-
self.assertIn('context', _context)
511-
self.assertIs(type(_context['context']['exception']),
512-
ZeroDivisionError)
513-
514464
def test_set_task_factory_invalid(self):
515465
with self.assertRaisesRegex(
516466
TypeError,
@@ -663,7 +613,7 @@ def test_loop_create_future(self):
663613
fut.cancel()
664614

665615
def test_loop_call_soon_handle_cancelled(self):
666-
cb = lambda: False
616+
cb = lambda: False # NoQA
667617
handle = self.loop.call_soon(cb)
668618
self.assertFalse(handle.cancelled())
669619
handle.cancel()
@@ -675,7 +625,7 @@ def test_loop_call_soon_handle_cancelled(self):
675625
self.assertFalse(handle.cancelled())
676626

677627
def test_loop_call_later_handle_cancelled(self):
678-
cb = lambda: False
628+
cb = lambda: False # NoQA
679629
handle = self.loop.call_later(0.01, cb)
680630
self.assertFalse(handle.cancelled())
681631
handle.cancel()
@@ -692,6 +642,58 @@ def test_loop_std_files_cloexec(self):
692642
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
693643
self.assertFalse(flags & fcntl.FD_CLOEXEC)
694644

645+
def test_default_exc_handler_broken(self):
646+
logger = logging.getLogger('asyncio')
647+
_context = None
648+
649+
class Loop(uvloop.Loop):
650+
651+
_selector = mock.Mock()
652+
_process_events = mock.Mock()
653+
654+
def default_exception_handler(self, context):
655+
nonlocal _context
656+
_context = context
657+
# Simulates custom buggy "default_exception_handler"
658+
raise ValueError('spam')
659+
660+
loop = Loop()
661+
self.addCleanup(loop.close)
662+
self.addCleanup(lambda: asyncio.set_event_loop(None))
663+
664+
asyncio.set_event_loop(loop)
665+
666+
def run_loop():
667+
def zero_error():
668+
loop.stop()
669+
1 / 0
670+
loop.call_soon(zero_error)
671+
loop.run_forever()
672+
673+
with mock.patch.object(logger, 'error') as log:
674+
run_loop()
675+
log.assert_called_with(
676+
'Exception in default exception handler',
677+
exc_info=True)
678+
679+
def custom_handler(loop, context):
680+
raise ValueError('ham')
681+
682+
_context = None
683+
loop.set_exception_handler(custom_handler)
684+
with mock.patch.object(logger, 'error') as log:
685+
run_loop()
686+
log.assert_called_with(
687+
self.mock_pattern('Exception in default exception.*'
688+
'while handling.*in custom'),
689+
exc_info=True)
690+
691+
# Check that original context was passed to default
692+
# exception handler.
693+
self.assertIn('context', _context)
694+
self.assertIs(type(_context['context']['exception']),
695+
ZeroDivisionError)
696+
695697

696698
class TestBaseAIO(_TestBase, AIOTestCase):
697699
pass

tests/test_signals.py

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import subprocess
44
import sys
55
import time
6+
import uvloop
67

78
from uvloop import _testbase as tb
89

@@ -277,6 +278,38 @@ async def coro(): pass
277278
class Test_UV_Signals(_TestSignal, tb.UVTestCase):
278279
NEW_LOOP = 'uvloop.new_event_loop()'
279280

281+
def test_signals_no_SIGCHLD(self):
282+
with self.assertRaisesRegex(RuntimeError,
283+
r"cannot add.*handler.*SIGCHLD"):
284+
285+
self.loop.add_signal_handler(signal.SIGCHLD, lambda *a: None)
286+
287+
def test_asyncio_add_watcher_SIGCHLD_nop(self):
288+
async def proc(loop):
289+
proc = await asyncio.create_subprocess_exec(
290+
'echo',
291+
stdout=subprocess.DEVNULL,
292+
loop=loop)
293+
await proc.wait()
294+
295+
aio_loop = asyncio.new_event_loop()
296+
asyncio.set_event_loop(aio_loop)
297+
try:
298+
aio_loop.run_until_complete(proc(aio_loop))
299+
finally:
300+
aio_loop.close()
301+
asyncio.set_event_loop(None)
302+
303+
try:
304+
loop = uvloop.new_event_loop()
305+
with self.assertWarnsRegex(
306+
RuntimeWarning,
307+
"asyncio is trying to install its ChildWatcher"):
308+
asyncio.set_event_loop(loop)
309+
finally:
310+
asyncio.set_event_loop(None)
311+
loop.close()
312+
280313

281314
class Test_AIO_Signals(_TestSignal, tb.AIOTestCase):
282315
NEW_LOOP = 'asyncio.new_event_loop()'

uvloop/includes/stdlib.pxi

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ cdef aio_isfuture = getattr(asyncio, 'isfuture', None)
4242
cdef aio_get_running_loop = getattr(asyncio, '_get_running_loop', None)
4343
cdef aio_set_running_loop = getattr(asyncio, '_set_running_loop', None)
4444
cdef aio_debug_wrapper = getattr(asyncio.coroutines, 'debug_wrapper', None)
45+
cdef aio_AbstractChildWatcher = asyncio.AbstractChildWatcher
4546

4647
cdef col_deque = collections.deque
4748
cdef col_Iterable = collections.abc.Iterable

uvloop/includes/uv.pxd

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ cdef extern from "uv.h" nogil:
6868

6969
cdef int SIGINT
7070
cdef int SIGHUP
71+
cdef int SIGCHLD
7172
cdef int SIGKILL
7273
cdef int SIGTERM
7374

uvloop/loop.pyx

+41-10
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ include "includes/stdlib.pxi"
4040

4141
include "errors.pyx"
4242

43-
cdef int PY37
44-
PY37 = PY_VERSION_HEX >= 0x03070000
43+
cdef:
44+
int PY37 = PY_VERSION_HEX >= 0x03070000
45+
int PY36 = PY_VERSION_HEX >= 0x03060000
4546

4647

4748
cdef _is_sock_stream(sock_type):
@@ -1034,19 +1035,19 @@ cdef class Loop:
10341035

10351036
if enabled:
10361037
if current_wrapper not in (None, wrapper):
1037-
warnings.warn(
1038+
_warn_with_source(
10381039
"loop.set_debug(True): cannot set debug coroutine "
10391040
"wrapper; another wrapper is already set %r" %
1040-
current_wrapper, RuntimeWarning)
1041+
current_wrapper, RuntimeWarning, self)
10411042
else:
10421043
sys_set_coroutine_wrapper(wrapper)
10431044
self._coroutine_debug_set = True
10441045
else:
10451046
if current_wrapper not in (None, wrapper):
1046-
warnings.warn(
1047+
_warn_with_source(
10471048
"loop.set_debug(False): cannot unset debug coroutine "
10481049
"wrapper; another wrapper was set %r" %
1049-
current_wrapper, RuntimeWarning)
1050+
current_wrapper, RuntimeWarning, self)
10501051
else:
10511052
sys_set_coroutine_wrapper(None)
10521053
self._coroutine_debug_set = False
@@ -2547,8 +2548,31 @@ cdef class Loop:
25472548

25482549
if (aio_iscoroutine(callback)
25492550
or aio_iscoroutinefunction(callback)):
2550-
raise TypeError("coroutines cannot be used "
2551-
"with add_signal_handler()")
2551+
raise TypeError(
2552+
"coroutines cannot be used with add_signal_handler()")
2553+
2554+
if sig == uv.SIGCHLD:
2555+
if (hasattr(callback, '__self__') and
2556+
isinstance(callback.__self__, aio_AbstractChildWatcher)):
2557+
2558+
_warn_with_source(
2559+
"!!! asyncio is trying to install its ChildWatcher for "
2560+
"SIGCHLD signal !!!\n\nThis is probably because a uvloop "
2561+
"instance is used with asyncio.set_event_loop(). "
2562+
"The correct way to use uvloop is to install its policy: "
2563+
"`asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())`"
2564+
"\n\n", RuntimeWarning, self)
2565+
2566+
# TODO: ideally we should always raise an error here,
2567+
# but that would be a backwards incompatible change,
2568+
# because we recommended using "asyncio.set_event_loop()"
2569+
# in our README. Need to start a deprecation period
2570+
# at some point to turn this warning into an error.
2571+
return
2572+
2573+
raise RuntimeError(
2574+
'cannot add a signal handler for SIGCHLD: it is used '
2575+
'by the event loop to track subprocesses')
25522576

25532577
self._check_signal(sig)
25542578
self._check_closed()
@@ -2771,10 +2795,10 @@ cdef class Loop:
27712795

27722796
def _asyncgen_firstiter_hook(self, agen):
27732797
if self._asyncgens_shutdown_called:
2774-
warnings_warn(
2798+
_warn_with_source(
27752799
"asynchronous generator {!r} was scheduled after "
27762800
"loop.shutdown_asyncgens() call".format(agen),
2777-
ResourceWarning, source=self)
2801+
ResourceWarning, self)
27782802

27792803
self._asyncgens.add(agen)
27802804

@@ -2909,6 +2933,13 @@ cdef _set_signal_wakeup_fd(fd):
29092933
signal_set_wakeup_fd(fd)
29102934

29112935

2936+
cdef _warn_with_source(msg, cls, source):
2937+
if PY36:
2938+
warnings_warn(msg, cls, source=source)
2939+
else:
2940+
warnings_warn(msg, cls)
2941+
2942+
29122943
########### Stuff for tests:
29132944

29142945
@cython.iterable_coroutine

0 commit comments

Comments
 (0)