diff --git a/Lib/selectors.py b/Lib/selectors.py index bb15a1cb1b..c3b065b522 100644 --- a/Lib/selectors.py +++ b/Lib/selectors.py @@ -50,12 +50,11 @@ SelectorKey.__doc__ = """SelectorKey(fileobj, fd, events, data) Object used to associate a file object to its backing file descriptor, selected event mask, and attached data. """ -if sys.version_info >= (3, 5): - SelectorKey.fileobj.__doc__ = 'File object registered.' - SelectorKey.fd.__doc__ = 'Underlying file descriptor.' - SelectorKey.events.__doc__ = 'Events that must be waited for on this file object.' - SelectorKey.data.__doc__ = ('''Optional opaque data associated to this file object. - For example, this could be used to store a per-client session ID.''') +SelectorKey.fileobj.__doc__ = 'File object registered.' +SelectorKey.fd.__doc__ = 'Underlying file descriptor.' +SelectorKey.events.__doc__ = 'Events that must be waited for on this file object.' +SelectorKey.data.__doc__ = ('''Optional opaque data associated to this file object. +For example, this could be used to store a per-client session ID.''') class _SelectorMapping(Mapping): @@ -510,6 +509,7 @@ if hasattr(select, 'kqueue'): def __init__(self): super().__init__() self._selector = select.kqueue() + self._max_events = 0 def fileno(self): return self._selector.fileno() @@ -521,10 +521,12 @@ if hasattr(select, 'kqueue'): kev = select.kevent(key.fd, select.KQ_FILTER_READ, select.KQ_EV_ADD) self._selector.control([kev], 0, 0) + self._max_events += 1 if events & EVENT_WRITE: kev = select.kevent(key.fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD) self._selector.control([kev], 0, 0) + self._max_events += 1 except: super().unregister(fileobj) raise @@ -535,6 +537,7 @@ if hasattr(select, 'kqueue'): if key.events & EVENT_READ: kev = select.kevent(key.fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE) + self._max_events -= 1 try: self._selector.control([kev], 0, 0) except OSError: @@ -544,6 +547,7 @@ if hasattr(select, 'kqueue'): if key.events & EVENT_WRITE: kev = select.kevent(key.fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE) + self._max_events -= 1 try: self._selector.control([kev], 0, 0) except OSError: @@ -556,7 +560,7 @@ if hasattr(select, 'kqueue'): # If max_ev is 0, kqueue will ignore the timeout. For consistent # behavior with the other selector classes, we prevent that here # (using max). See https://bugs.python.org/issue29255 - max_ev = max(len(self._fd_to_key), 1) + max_ev = self._max_events or 1 ready = [] try: kev_list = self._selector.control(None, max_ev, timeout) diff --git a/Lib/test/test_selectors.py b/Lib/test/test_selectors.py index 42c5e8879b..af730a8dec 100644 --- a/Lib/test/test_selectors.py +++ b/Lib/test/test_selectors.py @@ -19,6 +19,10 @@ except ImportError: resource = None +if support.is_emscripten or support.is_wasi: + raise unittest.SkipTest("Cannot create socketpair on Emscripten/WASI.") + + if hasattr(socket, 'socketpair'): socketpair = socket.socketpair else: @@ -276,6 +280,35 @@ class BaseSelectorTestCase: self.assertEqual([(wr_key, selectors.EVENT_WRITE)], result) + def test_select_read_write(self): + # gh-110038: when a file descriptor is registered for both read and + # write, the two events must be seen on a single call to select(). + s = self.SELECTOR() + self.addCleanup(s.close) + + sock1, sock2 = self.make_socketpair() + sock2.send(b"foo") + my_key = s.register(sock1, selectors.EVENT_READ | selectors.EVENT_WRITE) + + seen_read, seen_write = False, False + result = s.select() + # We get the read and write either in the same result entry or in two + # distinct entries with the same key. + self.assertLessEqual(len(result), 2) + for key, events in result: + self.assertTrue(isinstance(key, selectors.SelectorKey)) + self.assertEqual(key, my_key) + self.assertFalse(events & ~(selectors.EVENT_READ | + selectors.EVENT_WRITE)) + if events & selectors.EVENT_READ: + self.assertFalse(seen_read) + seen_read = True + if events & selectors.EVENT_WRITE: + self.assertFalse(seen_write) + seen_write = True + self.assertTrue(seen_read) + self.assertTrue(seen_write) + def test_context_manager(self): s = self.SELECTOR() self.addCleanup(s.close) @@ -446,6 +479,7 @@ class ScalableSelectorMixIn: # see issue #18963 for why it's skipped on older OS X versions @support.requires_mac_ver(10, 5) @unittest.skipUnless(resource, "Test needs resource module") + @support.requires_resource('cpu') def test_above_fd_setsize(self): # A scalable implementation should have no problem with more than # FD_SETSIZE file descriptors. Since we don't know the value, we just