]> granicus.if.org Git - python/commitdiff
bpo-23057: add loop self socket as wakeup fd for signals (#11135)
authorVladimir Matveev <v2matveev@outlook.com>
Tue, 18 Dec 2018 21:56:17 +0000 (13:56 -0800)
committerAndrew Svetlov <andrew.svetlov@gmail.com>
Tue, 18 Dec 2018 21:56:17 +0000 (23:56 +0200)
Lib/asyncio/proactor_events.py
Lib/asyncio/windows_events.py
Lib/test/test_asyncio/test_ctrl_c_in_proactor_loop_helper.py [new file with mode: 0644]
Lib/test/test_asyncio/test_proactor_events.py
Lib/test/test_asyncio/test_windows_events.py
Misc/NEWS.d/next/Library/2018-12-12-16-24-55.bpo-23057.OB4Z1Y.rst [new file with mode: 0644]

index 69d96a81035e04624198fd6a9f6c1101c11b8de8..9b9e0aa7c7f1e2d606f0524d07449ed7ba8e98ba 100644 (file)
@@ -10,6 +10,7 @@ import io
 import os
 import socket
 import warnings
+import signal
 
 from . import base_events
 from . import constants
@@ -489,6 +490,8 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
         self._accept_futures = {}   # socket file descriptor => Future
         proactor.set_loop(self)
         self._make_self_pipe()
+        self_no = self._csock.fileno()
+        signal.set_wakeup_fd(self_no)
 
     def _make_socket_transport(self, sock, protocol, waiter=None,
                                extra=None, server=None):
@@ -529,6 +532,7 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
         if self.is_closed():
             return
 
+        signal.set_wakeup_fd(-1)
         # 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.
@@ -613,7 +617,6 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
         self._ssock.setblocking(False)
         self._csock.setblocking(False)
         self._internal_fds += 1
-        self.call_soon(self._loop_self_reading)
 
     def _loop_self_reading(self, f=None):
         try:
index 772ddf4dfebd518826c7f2006bb4cc8880f0ad46..33ffaf97177069a03179cf90051dcdc384732f6d 100644 (file)
@@ -308,6 +308,16 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
             proactor = IocpProactor()
         super().__init__(proactor)
 
+    def run_forever(self):
+        try:
+            assert self._self_reading_future is None
+            self.call_soon(self._loop_self_reading)
+            super().run_forever()
+        finally:
+            if self._self_reading_future is not None:
+                self._self_reading_future.cancel()
+                self._self_reading_future = None
+
     async def create_pipe_connection(self, protocol_factory, address):
         f = self._proactor.connect_pipe(address)
         pipe = await f
diff --git a/Lib/test/test_asyncio/test_ctrl_c_in_proactor_loop_helper.py b/Lib/test/test_asyncio/test_ctrl_c_in_proactor_loop_helper.py
new file mode 100644 (file)
index 0000000..9aeb58a
--- /dev/null
@@ -0,0 +1,63 @@
+import sys
+
+
+def do_in_child_process():
+    import asyncio
+
+    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
+    l = asyncio.get_event_loop()
+
+    def step(n):
+        try:
+            print(n)
+            sys.stdout.flush()
+            l.run_forever()
+            sys.exit(100)
+        except KeyboardInterrupt:
+            # ok
+            pass
+        except:
+            # error - use default exit code
+            sys.exit(200)
+
+    step(1)
+    step(2)
+    sys.exit(255)
+
+
+def do_in_main_process():
+    import os
+    import signal
+    import subprocess
+    import time
+    from test.support.script_helper import spawn_python
+
+    ok = False
+
+    def step(p, expected):
+        s = p.stdout.readline()
+        if s != expected:
+            raise Exception(f"Unexpected line: got {s}, expected '{expected}'")
+        # ensure that child process gets to run_forever
+        time.sleep(0.5)
+        os.kill(p.pid, signal.CTRL_C_EVENT)
+
+    with spawn_python(__file__, "--child") as p:
+        try:
+            # ignore ctrl-c in current process
+            signal.signal(signal.SIGINT, signal.SIG_IGN)
+            step(p, b"1\r\n")
+            step(p, b"2\r\n")
+            exit_code = p.wait(timeout=5)
+            ok = exit_code = 255
+        except Exception as e:
+            sys.stderr.write(repr(e))
+            p.kill()
+    sys.exit(255 if ok else 1)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) == 1:
+        do_in_main_process()
+    else:
+        do_in_child_process()
index afc4c19a96b0b1ef2489a468cd33a1b1d89b94ce..5952ccccce0e5d4a4c666a428e74583680e88bb6 100644 (file)
@@ -737,19 +737,19 @@ class BaseProactorEventLoopTests(test_utils.TestCase):
 
         with mock.patch('asyncio.proactor_events.socket.socketpair',
                         return_value=(self.ssock, self.csock)):
-            self.loop = BaseProactorEventLoop(self.proactor)
+            with mock.patch('signal.set_wakeup_fd'):
+                self.loop = BaseProactorEventLoop(self.proactor)
         self.set_event_loop(self.loop)
 
-    @mock.patch.object(BaseProactorEventLoop, 'call_soon')
     @mock.patch('asyncio.proactor_events.socket.socketpair')
-    def test_ctor(self, socketpair, call_soon):
+    def test_ctor(self, socketpair):
         ssock, csock = socketpair.return_value = (
             mock.Mock(), mock.Mock())
-        loop = BaseProactorEventLoop(self.proactor)
+        with mock.patch('signal.set_wakeup_fd'):
+            loop = BaseProactorEventLoop(self.proactor)
         self.assertIs(loop._ssock, ssock)
         self.assertIs(loop._csock, csock)
         self.assertEqual(loop._internal_fds, 1)
-        call_soon.assert_called_with(loop._loop_self_reading)
         loop.close()
 
     def test_close_self_pipe(self):
index 8f4c50e2c92b453434db141cca619bbfef63c209..05d875ac3c8523363c5a6992f126acfdb52bfb78 100644 (file)
@@ -1,6 +1,9 @@
 import os
+import signal
 import socket
 import sys
+import subprocess
+import time
 import unittest
 from unittest import mock
 
@@ -13,6 +16,7 @@ import _winapi
 import asyncio
 from asyncio import windows_events
 from test.test_asyncio import utils as test_utils
+from test.support.script_helper import spawn_python
 
 
 def tearDownModule():
@@ -33,6 +37,23 @@ class UpperProto(asyncio.Protocol):
             self.trans.close()
 
 
+class ProactorLoopCtrlC(test_utils.TestCase):
+    def test_ctrl_c(self):
+        from .test_ctrl_c_in_proactor_loop_helper import __file__ as f
+
+        # ctrl-c will be sent to all processes that share the same console
+        # in order to isolate the effect of raising ctrl-c we'll create
+        # a process with a new console
+        flags = subprocess.CREATE_NEW_CONSOLE
+        with spawn_python(f, creationflags=flags) as p:
+            try:
+                exit_code = p.wait(timeout=5)
+                self.assertEqual(exit_code, 255)
+            except:
+                p.kill()
+                raise
+
+
 class ProactorTests(test_utils.TestCase):
 
     def setUp(self):
diff --git a/Misc/NEWS.d/next/Library/2018-12-12-16-24-55.bpo-23057.OB4Z1Y.rst b/Misc/NEWS.d/next/Library/2018-12-12-16-24-55.bpo-23057.OB4Z1Y.rst
new file mode 100644 (file)
index 0000000..51b727d
--- /dev/null
@@ -0,0 +1 @@
+Unblock Proactor event loop when keyboard interrupt is received on Windows