diff --git a/Lib/socket.py b/Lib/socket.py index dbb7cca67d6..0b19e30f91b 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -54,6 +54,8 @@ except ImportError: errno = None EBADF = getattr(errno, 'EBADF', 9) EINTR = getattr(errno, 'EINTR', 4) +EAGAIN = getattr(errno, 'EAGAIN', 11) +EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) __all__ = ["getfqdn", "create_connection"] __all__.extend(os._get_exports_list(_socket)) @@ -220,6 +222,8 @@ if hasattr(_socket, "socketpair"): return a, b +_blocking_errnos = { EAGAIN, EWOULDBLOCK } + class SocketIO(io.RawIOBase): """Raw I/O implementation for stream sockets. @@ -262,8 +266,11 @@ class SocketIO(io.RawIOBase): try: return self._sock.recv_into(b) except error as e: - if e.args[0] == EINTR: + n = e.args[0] + if n == EINTR: continue + if n in _blocking_errnos: + return None raise def write(self, b): @@ -274,7 +281,13 @@ class SocketIO(io.RawIOBase): """ self._checkClosed() self._checkWritable() - return self._sock.send(b) + try: + return self._sock.send(b) + except error as e: + # XXX what about EINTR? + if e.args[0] in _blocking_errnos: + return None + raise def readable(self): """True if the SocketIO is open for reading. diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 718ea5c595d..3ef0850cd79 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -137,8 +137,8 @@ class ThreadableTest: self.done.wait() if self.queue.qsize(): - msg = self.queue.get() - self.fail(msg) + exc = self.queue.get() + raise exc def clientRun(self, test_func): self.server_ready.wait() @@ -148,9 +148,10 @@ class ThreadableTest: raise TypeError("test_func must be a callable function") try: test_func() - except Exception as strerror: - self.queue.put(strerror) - self.clientTearDown() + except BaseException as e: + self.queue.put(e) + finally: + self.clientTearDown() def clientSetUp(self): raise NotImplementedError("clientSetUp must be implemented.") @@ -932,10 +933,13 @@ class FileObjectClassTestCase(SocketConnectedTest): SocketConnectedTest.__init__(self, methodName=methodName) def setUp(self): + self.evt1, self.evt2, self.serv_finished, self.cli_finished = [ + threading.Event() for i in range(4)] SocketConnectedTest.setUp(self) self.serv_file = self.cli_conn.makefile('rb', self.bufsize) def tearDown(self): + self.serv_finished.set() self.serv_file.close() self.assertTrue(self.serv_file.closed) self.serv_file = None @@ -943,9 +947,10 @@ class FileObjectClassTestCase(SocketConnectedTest): def clientSetUp(self): SocketConnectedTest.clientSetUp(self) - self.cli_file = self.serv_conn.makefile('wb') + self.cli_file = self.serv_conn.makefile('wb', self.bufsize) def clientTearDown(self): + self.cli_finished.set() self.cli_file.close() self.assertTrue(self.cli_file.closed) self.cli_file = None @@ -1196,6 +1201,62 @@ class UnbufferedFileObjectClassTestCase(FileObjectClassTestCase): def _testMakefileCloseSocketDestroy(self): pass + # Non-blocking ops + # NOTE: to set `serv_file` as non-blocking, we must call + # `cli_conn.setblocking` and vice-versa (see setUp / clientSetUp). + + def testSmallReadNonBlocking(self): + self.cli_conn.setblocking(False) + self.assertEqual(self.serv_file.readinto(bytearray(10)), None) + self.assertEqual(self.serv_file.read(len(MSG) - 3), None) + self.evt1.set() + self.evt2.wait(1.0) + first_seg = self.serv_file.read(len(MSG) - 3) + buf = bytearray(10) + n = self.serv_file.readinto(buf) + self.assertEqual(n, 3) + msg = first_seg + buf[:n] + self.assertEqual(msg, MSG) + self.assertEqual(self.serv_file.readinto(bytearray(16)), None) + self.assertEqual(self.serv_file.read(1), None) + + def _testSmallReadNonBlocking(self): + self.evt1.wait(1.0) + self.cli_file.write(MSG) + self.cli_file.flush() + self.evt2.set() + # Avoid cloding the socket before the server test has finished, + # otherwise system recv() will return 0 instead of EWOULDBLOCK. + self.serv_finished.wait(5.0) + + def testWriteNonBlocking(self): + self.cli_finished.wait(5.0) + # The client thread can't skip directly - the SkipTest exception + # would appear as a failure. + if self.serv_skipped: + self.skipTest(self.serv_skipped) + + def _testWriteNonBlocking(self): + self.serv_skipped = None + self.serv_conn.setblocking(False) + # Try to saturate the socket buffer pipe with repeated large writes. + BIG = b"x" * (1024 ** 2) + LIMIT = 10 + # The first write() succeeds since a chunk of data can be buffered + n = self.cli_file.write(BIG) + self.assertGreater(n, 0) + for i in range(LIMIT): + n = self.cli_file.write(BIG) + if n is None: + # Succeeded + break + self.assertGreater(n, 0) + else: + # Let us know that this test didn't manage to establish + # the expected conditions. This is not a failure in itself but, + # if it happens repeatedly, the test should be fixed. + self.serv_skipped = "failed to saturate the socket buffer" + class LineBufferedFileObjectClassTestCase(FileObjectClassTestCase): diff --git a/Misc/NEWS b/Misc/NEWS index c1a5bc8e08f..34cea645f70 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -52,6 +52,10 @@ Core and Builtins Library ------- +- Issue #9854: SocketIO objects now observe the RawIOBase interface in + non-blocking mode: they return None when an operation would block (instead + of raising an exception). + - Issue #1730136: Fix the comparison between a tk.font.Font and an object of another kind.