Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion Doc/library/signal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,41 @@ The :mod:`!signal` module defines the following functions:
.. availability:: Unix.


.. function:: set_wakeup_fd(fd, *, warn_on_full_buffer=True)
.. function:: get_wakeup()

Return a :class:`wakeup` snapshot of the current signal wakeup
configuration without changing it. This can be used with
:func:`set_wakeup_fd` to update the process-wide wakeup configuration only
if another owner has not changed it.

When threads are enabled, this function can only be called from
:ref:`the main thread of the main interpreter <signals-and-threads>`;
attempting to call it from other threads will cause a :exc:`ValueError`
exception to be raised.

.. versionadded:: 3.16


.. class:: wakeup

An immutable snapshot of the signal wakeup configuration. It has these
read-only attributes:

.. attribute:: wakeup.fd

The wakeup file descriptor, or ``-1`` if wakeups are disabled.

.. attribute:: wakeup.warn_on_full_buffer

Whether a full wakeup buffer reports warnings.

.. attribute:: wakeup.is_socket

Whether *fd* is used as a socket on Windows. This is always false on
other platforms.


.. function:: set_wakeup_fd(fd, *, warn_on_full_buffer=True, check_previous=None)

Set the wakeup file descriptor to *fd*. When a signal your program has
registered a signal handler for is received, the signal number is written as
Expand All @@ -562,6 +596,11 @@ The :mod:`!signal` module defines the following functions:
If not -1, *fd* must be non-blocking. It is up to the library to remove
any bytes from *fd* before calling poll or select again.

If *check_previous* is a :class:`wakeup` snapshot, change the wakeup
configuration only if it is still equal to the current configuration.
Otherwise, raise :exc:`RuntimeError` without changing it. Pass ``None``
(the default) to update unconditionally.

When threads are enabled, this function can only be called
from :ref:`the main thread of the main interpreter <signals-and-threads>`;
attempting to call it from other threads will cause a :exc:`ValueError`
Expand Down Expand Up @@ -594,6 +633,9 @@ The :mod:`!signal` module defines the following functions:
.. versionchanged:: 3.7
Added ``warn_on_full_buffer`` parameter.

.. versionchanged:: 3.16
Added the ``check_previous`` parameter.

.. function:: siginterrupt(signalnum, flag)

Change system call restart behaviour: if *flag* is :const:`False`, system
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(cb_type)
STRUCT_FOR_ID(certfile)
STRUCT_FOR_ID(chain)
STRUCT_FOR_ID(check_previous)
STRUCT_FOR_ID(check_same_thread)
STRUCT_FOR_ID(clamp)
STRUCT_FOR_ID(clear)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions Lib/asyncio/proactor_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,11 +663,13 @@ def __init__(self, proactor):
self._selector = proactor # convenient alias
self._self_reading_future = None
self._accept_futures = {} # socket file descriptor => Future
self._wakeup = None
proactor.set_loop(self)
self._make_self_pipe()
if threading.current_thread() is threading.main_thread():
# wakeup fd can only be installed to a file descriptor from the main thread
signal.set_wakeup_fd(self._csock.fileno())
self._wakeup = signal.get_wakeup()

def _make_socket_transport(self, sock, protocol, waiter=None,
extra=None, server=None, context=None):
Expand Down Expand Up @@ -715,8 +717,14 @@ def close(self):
if self.is_closed():
return

if threading.current_thread() is threading.main_thread():
signal.set_wakeup_fd(-1)
if (threading.current_thread() is threading.main_thread()
and self._wakeup is not None):
try:
signal.set_wakeup_fd(-1, check_previous=self._wakeup)
except RuntimeError:
# Another loop or host replaced the process-global wakeup fd.
pass
self._wakeup = None
# Call these methods before closing the event loop (before calling
# BaseEventLoop.close), because they can schedule callbacks with
# call_soon(), which is forbidden when the event loop is closed.
Expand Down
9 changes: 7 additions & 2 deletions Lib/test/test_asyncio/test_proactor_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,19 +768,24 @@ def setUp(self):

with mock.patch('asyncio.proactor_events.socket.socketpair',
return_value=(self.ssock, self.csock)):
with mock.patch('signal.set_wakeup_fd'):
with mock.patch('signal.set_wakeup_fd'), \
mock.patch('signal.get_wakeup'):
self.loop = BaseProactorEventLoop(self.proactor)
self.loop._wakeup = None
self.set_event_loop(self.loop)

@mock.patch('asyncio.proactor_events.socket.socketpair')
def test_ctor(self, socketpair):
ssock, csock = socketpair.return_value = (
mock.Mock(), mock.Mock())
with mock.patch('signal.set_wakeup_fd'):
with mock.patch('signal.set_wakeup_fd'), \
mock.patch('signal.get_wakeup'):
loop = BaseProactorEventLoop(self.proactor)
self.assertIs(loop._ssock, ssock)
self.assertIs(loop._csock, csock)
self.assertEqual(loop._internal_fds, 1)
# The wakeup state is mocked in this unit test.
loop._wakeup = None
loop.close()

def test_close_self_pipe(self):
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_asyncio/test_windows_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,29 @@ def SIGINT_after_delay():
thread.join()


class ProactorWakeupOwnershipTests(WindowsEventsTestCase):

def test_close_preserves_newer_loop_wakeup(self):
initial = signal.get_wakeup()
first = asyncio.ProactorEventLoop()
second = asyncio.ProactorEventLoop()
try:
newer_wakeup = signal.get_wakeup()
self.assertEqual(newer_wakeup.fd, second._csock.fileno())

first.close()
self.assertEqual(signal.get_wakeup(), newer_wakeup)
finally:
if not first.is_closed():
first.close()
if not second.is_closed():
second.close()
signal.set_wakeup_fd(
initial.fd,
warn_on_full_buffer=initial.warn_on_full_buffer,
)


class ProactorMultithreading(WindowsEventsTestCase):
def test_run_from_nonmain_thread(self):
finished = False
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,48 @@ def test_set_wakeup_fd_socket_result(self):
self.assertEqual(signal.set_wakeup_fd(-1), fd2)
self.assertEqual(signal.set_wakeup_fd(-1), -1)

@unittest.skipUnless(support.has_socket_support, "needs working sockets.")
def test_wakeup_state_and_conditional_update(self):
first, second = socket.socketpair()
self.addCleanup(first.close)
self.addCleanup(second.close)
first.setblocking(False)
second.setblocking(False)
self.addCleanup(signal.set_wakeup_fd, -1)

initial_state = signal.get_wakeup()
self.assertEqual(initial_state.fd, -1)
self.assertFalse(initial_state.is_socket)
signal.set_wakeup_fd(first.fileno(), warn_on_full_buffer=False)
first_state = signal.get_wakeup()
self.assertEqual(first_state.fd, first.fileno())
self.assertFalse(first_state.warn_on_full_buffer)
self.assertEqual(first_state.is_socket, sys.platform == "win32")

signal.set_wakeup_fd(
first.fileno(),
warn_on_full_buffer=True,
check_previous=first_state,
)
changed_state = signal.get_wakeup()
self.assertEqual(changed_state.fd, first.fileno())
self.assertTrue(changed_state.warn_on_full_buffer)

with self.assertRaisesRegex(RuntimeError,
"signal wakeup configuration changed"):
signal.set_wakeup_fd(second.fileno(), check_previous=first_state)
self.assertEqual(signal.get_wakeup(), changed_state)

signal.set_wakeup_fd(second.fileno(), check_previous=changed_state)
second_state = signal.get_wakeup()
self.assertEqual(second_state.fd, second.fileno())
self.assertTrue(second_state.warn_on_full_buffer)

with self.assertRaisesRegex(RuntimeError,
"signal wakeup configuration changed"):
signal.set_wakeup_fd(-1, check_previous=first_state)
self.assertEqual(signal.get_wakeup(), second_state)

# On Windows, files are always blocking and Windows does not provide a
# function to test if a socket is in non-blocking mode.
@unittest.skipIf(sys.platform == "win32", "tests specific to POSIX")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :func:`signal.get_wakeup` and conditional ``check_previous`` support
to :func:`signal.set_wakeup_fd`, allowing cooperating event loops and
hosts to avoid overwriting each other's signal wakeup configuration.
51 changes: 39 additions & 12 deletions Modules/clinic/signalmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading