diff --git a/Doc/library/os.rst b/Doc/library/os.rst index c03ce65f21a..9cfc4729408 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -807,6 +807,17 @@ as internal buffering of data. Availability: Unix. +.. function:: get_blocking(fd) + + Get the blocking mode of the file descriptor: ``False`` if the + :data:`O_NONBLOCK` flag is set, ``True`` if the flag is cleared. + + See also :func:`set_blocking` and :meth:`socket.socket.setblocking`. + + Availability: Unix. + + .. versionadded:: 3.5 + .. function:: isatty(fd) Return ``True`` if the file descriptor *fd* is open and connected to a @@ -1107,6 +1118,18 @@ or `the MSDN `_ on Window .. versionadded:: 3.3 +.. function:: set_blocking(fd, blocking) + + Set the blocking mode of the specified file descriptor. Set the + :data:`O_NONBLOCK` flag if blocking is ``False``, clear the flag otherwise. + + See also :func:`get_blocking` and :meth:`socket.socket.setblocking`. + + Availability: Unix. + + .. versionadded:: 3.5 + + .. data:: SF_NODISKIO SF_MNOWAIT SF_SYNC diff --git a/Include/fileutils.h b/Include/fileutils.h index e9bad80b873..f2a43f75c4f 100644 --- a/Include/fileutils.h +++ b/Include/fileutils.h @@ -70,7 +70,14 @@ PyAPI_FUNC(int) _Py_set_inheritable(int fd, int inheritable, int *atomic_flag_works); PyAPI_FUNC(int) _Py_dup(int fd); -#endif + +#ifndef MS_WINDOWS +PyAPI_FUNC(int) _Py_get_blocking(int fd); + +PyAPI_FUNC(int) _Py_set_blocking(int fd, int blocking); +#endif /* !MS_WINDOWS */ + +#endif /* Py_LIMITED_API */ #ifdef __cplusplus } diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 5020cc5db58..665901ad263 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -1,7 +1,6 @@ """Selector event loop for Unix with signal handling.""" import errno -import fcntl import os import signal import socket @@ -259,12 +258,6 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop): return server -def _set_nonblocking(fd): - flags = fcntl.fcntl(fd, fcntl.F_GETFL) - flags = flags | os.O_NONBLOCK - fcntl.fcntl(fd, fcntl.F_SETFL, flags) - - class _UnixReadPipeTransport(transports.ReadTransport): max_size = 256 * 1024 # max bytes we read in one event loop iteration @@ -280,7 +273,7 @@ class _UnixReadPipeTransport(transports.ReadTransport): stat.S_ISSOCK(mode) or stat.S_ISCHR(mode)): raise ValueError("Pipe transport is for pipes/sockets only.") - _set_nonblocking(self._fileno) + os.set_blocking(self._fileno, False) self._protocol = protocol self._closing = False self._loop.add_reader(self._fileno, self._read_ready) @@ -373,7 +366,7 @@ class _UnixWritePipeTransport(transports._FlowControlMixin, stat.S_ISCHR(mode)): raise ValueError("Pipe transport is only for " "pipes, sockets and character devices") - _set_nonblocking(self._fileno) + os.set_blocking(self._fileno, False) self._protocol = protocol self._buffer = [] self._conn_lost = 0 diff --git a/Lib/asyncore.py b/Lib/asyncore.py index 90854b22bb0..da24b3843b3 100644 --- a/Lib/asyncore.py +++ b/Lib/asyncore.py @@ -590,8 +590,6 @@ def close_all(map=None, ignore_all=False): # Regardless, this is useful for pipes, and stdin/stdout... if os.name == 'posix': - import fcntl - class file_wrapper: # Here we override just enough to make a file # look like a socket for the purposes of asyncore. @@ -642,9 +640,7 @@ if os.name == 'posix': pass self.set_file(fd) # set it to non-blocking mode - flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) - flags = flags | os.O_NONBLOCK - fcntl.fcntl(fd, fcntl.F_SETFL, flags) + os.set_blocking(fd, False) def set_file(self, fd): self.socket = file_wrapper(fd) diff --git a/Lib/test/test_asyncio/test_unix_events.py b/Lib/test/test_asyncio/test_unix_events.py index 099d4d51af4..d185533e88e 100644 --- a/Lib/test/test_asyncio/test_unix_events.py +++ b/Lib/test/test_asyncio/test_unix_events.py @@ -306,9 +306,9 @@ class UnixReadPipeTransportTests(test_utils.TestCase): self.pipe = mock.Mock(spec_set=io.RawIOBase) self.pipe.fileno.return_value = 5 - fcntl_patcher = mock.patch('fcntl.fcntl') - fcntl_patcher.start() - self.addCleanup(fcntl_patcher.stop) + blocking_patcher = mock.patch('os.set_blocking') + blocking_patcher.start() + self.addCleanup(blocking_patcher.stop) fstat_patcher = mock.patch('os.fstat') m_fstat = fstat_patcher.start() @@ -469,9 +469,9 @@ class UnixWritePipeTransportTests(test_utils.TestCase): self.pipe = mock.Mock(spec_set=io.RawIOBase) self.pipe.fileno.return_value = 5 - fcntl_patcher = mock.patch('fcntl.fcntl') - fcntl_patcher.start() - self.addCleanup(fcntl_patcher.stop) + blocking_patcher = mock.patch('os.set_blocking') + blocking_patcher.start() + self.addCleanup(blocking_patcher.stop) fstat_patcher = mock.patch('os.fstat') m_fstat = fstat_patcher.start() diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index 91ba5515447..ad86301fcd2 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -44,10 +44,6 @@ try: import threading except ImportError: threading = None -try: - import fcntl -except ImportError: - fcntl = None def _default_chunk_size(): """Get the default TextIOWrapper chunk size""" @@ -3230,26 +3226,20 @@ class MiscIOTest(unittest.TestCase): with self.open(support.TESTFN, **kwargs) as f: self.assertRaises(TypeError, pickle.dumps, f, protocol) - @unittest.skipUnless(fcntl, 'fcntl required for this test') def test_nonblock_pipe_write_bigbuf(self): self._test_nonblock_pipe_write(16*1024) - @unittest.skipUnless(fcntl, 'fcntl required for this test') def test_nonblock_pipe_write_smallbuf(self): self._test_nonblock_pipe_write(1024) - def _set_non_blocking(self, fd): - flags = fcntl.fcntl(fd, fcntl.F_GETFL) - self.assertNotEqual(flags, -1) - res = fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - self.assertEqual(res, 0) - + @unittest.skipUnless(hasattr(os, 'set_blocking'), + 'os.set_blocking() required for this test') def _test_nonblock_pipe_write(self, bufsize): sent = [] received = [] r, w = os.pipe() - self._set_non_blocking(r) - self._set_non_blocking(w) + os.set_blocking(r, False) + os.set_blocking(w, False) # To exercise all code paths in the C implementation we need # to play with buffer sizes. For instance, if we choose a diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index e669df83384..020d0fa41f1 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1376,6 +1376,16 @@ class TestInvalidFD(unittest.TestCase): def test_writev(self): self.check(os.writev, [b'abc']) + def test_inheritable(self): + self.check(os.get_inheritable) + self.check(os.set_inheritable, True) + + @unittest.skipUnless(hasattr(os, 'get_blocking'), + 'needs os.get_blocking() and os.set_blocking()') + def test_blocking(self): + self.check(os.get_blocking) + self.check(os.set_blocking, True) + class LinkTests(unittest.TestCase): def setUp(self): @@ -2591,6 +2601,21 @@ class FDInheritanceTests(unittest.TestCase): self.assertEqual(os.get_inheritable(slave_fd), False) +@unittest.skipUnless(hasattr(os, 'get_blocking'), + 'needs os.get_blocking() and os.set_blocking()') +class BlockingTests(unittest.TestCase): + def test_blocking(self): + fd = os.open(__file__, os.O_RDONLY) + self.addCleanup(os.close, fd) + self.assertEqual(os.get_blocking(fd), True) + + os.set_blocking(fd, False) + self.assertEqual(os.get_blocking(fd), False) + + os.set_blocking(fd, True) + self.assertEqual(os.get_blocking(fd), True) + + @support.reap_threads def test_main(): support.run_unittest( @@ -2626,6 +2651,7 @@ def test_main(): CPUCountTests, FDInheritanceTests, Win32JunctionTests, + BlockingTests, ) if __name__ == "__main__": diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 3fae2b187cb..d9acfa4fbc9 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -9,7 +9,6 @@ import errno import sys import time import os -import fcntl import platform import pwd import shutil @@ -355,7 +354,7 @@ class PosixTester(unittest.TestCase): def test_oscloexec(self): fd = os.open(support.TESTFN, os.O_RDONLY|os.O_CLOEXEC) self.addCleanup(os.close, fd) - self.assertTrue(fcntl.fcntl(fd, fcntl.F_GETFD) & fcntl.FD_CLOEXEC) + self.assertFalse(os.get_inheritable(fd)) @unittest.skipUnless(hasattr(posix, 'O_EXLOCK'), 'test needs posix.O_EXLOCK') @@ -605,8 +604,8 @@ class PosixTester(unittest.TestCase): self.addCleanup(os.close, w) self.assertFalse(os.get_inheritable(r)) self.assertFalse(os.get_inheritable(w)) - self.assertTrue(fcntl.fcntl(r, fcntl.F_GETFL) & os.O_NONBLOCK) - self.assertTrue(fcntl.fcntl(w, fcntl.F_GETFL) & os.O_NONBLOCK) + self.assertFalse(os.get_blocking(r)) + self.assertFalse(os.get_blocking(w)) # try reading from an empty pipe: this should fail, not block self.assertRaises(OSError, os.read, r, 1) # try a write big enough to fill-up the pipe: this should either diff --git a/Lib/test/test_pty.py b/Lib/test/test_pty.py index 8916861f5bb..9b783c37683 100644 --- a/Lib/test/test_pty.py +++ b/Lib/test/test_pty.py @@ -1,7 +1,6 @@ from test.support import verbose, run_unittest, import_module, reap_children -#Skip these tests if either fcntl or termios is not available -fcntl = import_module('fcntl') +# Skip these tests if termios is not available import_module('termios') import errno @@ -84,16 +83,18 @@ class PtyTest(unittest.TestCase): # in master_open(), we need to read the EOF. # Ensure the fd is non-blocking in case there's nothing to read. - orig_flags = fcntl.fcntl(master_fd, fcntl.F_GETFL) - fcntl.fcntl(master_fd, fcntl.F_SETFL, orig_flags | os.O_NONBLOCK) + blocking = os.get_blocking(master_fd) try: - s1 = os.read(master_fd, 1024) - self.assertEqual(b'', s1) - except OSError as e: - if e.errno != errno.EAGAIN: - raise - # Restore the original flags. - fcntl.fcntl(master_fd, fcntl.F_SETFL, orig_flags) + os.set_blocking(master_fd, False) + try: + s1 = os.read(master_fd, 1024) + self.assertEqual(b'', s1) + except OSError as e: + if e.errno != errno.EAGAIN: + raise + finally: + # Restore the original flags. + os.set_blocking(master_fd, blocking) debug("Writing to slave_fd") os.write(slave_fd, TEST_STRING_1) diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index ca571b9d532..caca0be999d 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -276,7 +276,6 @@ class WakeupSignalTests(unittest.TestCase): # use a subprocess to have only one thread code = """if 1: import _testcapi - import fcntl import os import signal import struct @@ -299,10 +298,7 @@ class WakeupSignalTests(unittest.TestCase): signal.signal(signal.SIGALRM, handler) read, write = os.pipe() - for fd in (read, write): - flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) - flags = flags | os.O_NONBLOCK - fcntl.fcntl(fd, fcntl.F_SETFL, flags) + os.set_blocking(write, False) signal.set_wakeup_fd(write) test() @@ -322,7 +318,6 @@ class WakeupSignalTests(unittest.TestCase): code = """if 1: import _testcapi import errno - import fcntl import os import signal import sys @@ -333,8 +328,7 @@ class WakeupSignalTests(unittest.TestCase): signal.signal(signal.SIGALRM, handler) r, w = os.pipe() - flags = fcntl.fcntl(r, fcntl.F_GETFL, 0) - fcntl.fcntl(r, fcntl.F_SETFL, flags | os.O_NONBLOCK) + os.set_blocking(r, False) # Set wakeup_fd a read-only file descriptor to trigger the error signal.set_wakeup_fd(r) diff --git a/Misc/NEWS b/Misc/NEWS index 2db6da096c8..1dd0c478550 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -113,6 +113,10 @@ Core and Builtins Library ------- +- Issue #22054: Add os.get_blocking() and os.set_blocking() functions to get + and set the blocking mode of a file descriptor (False if the O_NONBLOCK flag + is set, True otherwise). These functions are not available on Windows. + - Issue #17172: Make turtledemo start as active on Mac even when run with subprocess. Patch by Ned Daily and Lita Cho. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b7acbc330c7..0bee3c986d0 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11146,6 +11146,56 @@ posix_set_handle_inheritable(PyObject *self, PyObject *args) #endif /* MS_WINDOWS */ +#ifndef MS_WINDOWS +PyDoc_STRVAR(get_blocking__doc__, + "get_blocking(fd) -> bool\n" \ + "\n" \ + "Get the blocking mode of the file descriptor:\n" \ + "False if the O_NONBLOCK flag is set, True if the flag is cleared."); + +static PyObject* +posix_get_blocking(PyObject *self, PyObject *args) +{ + int fd; + int blocking; + + if (!PyArg_ParseTuple(args, "i:get_blocking", &fd)) + return NULL; + + if (!_PyVerify_fd(fd)) + return posix_error(); + + blocking = _Py_get_blocking(fd); + if (blocking < 0) + return NULL; + return PyBool_FromLong(blocking); +} + +PyDoc_STRVAR(set_blocking__doc__, + "set_blocking(fd, blocking)\n" \ + "\n" \ + "Set the blocking mode of the specified file descriptor.\n" \ + "Set the O_NONBLOCK flag if blocking is False,\n" \ + "clear the O_NONBLOCK flag otherwise."); + +static PyObject* +posix_set_blocking(PyObject *self, PyObject *args) +{ + int fd, blocking; + + if (!PyArg_ParseTuple(args, "ii:set_blocking", &fd, &blocking)) + return NULL; + + if (!_PyVerify_fd(fd)) + return posix_error(); + + if (_Py_set_blocking(fd, blocking) < 0) + return NULL; + Py_RETURN_NONE; +} +#endif /* !MS_WINDOWS */ + + /*[clinic input] dump buffer [clinic start generated code]*/ @@ -11605,6 +11655,10 @@ static PyMethodDef posix_methods[] = { METH_VARARGS, get_handle_inheritable__doc__}, {"set_handle_inheritable", posix_set_handle_inheritable, METH_VARARGS, set_handle_inheritable__doc__}, +#endif +#ifndef MS_WINDOWS + {"get_blocking", posix_get_blocking, METH_VARARGS, get_blocking__doc__}, + {"set_blocking", posix_set_blocking, METH_VARARGS, set_blocking__doc__}, #endif {NULL, NULL} /* Sentinel */ }; diff --git a/Python/fileutils.c b/Python/fileutils.c index a55064ffdc2..065d3fd9741 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1045,3 +1045,56 @@ _Py_dup(int fd) return fd; } +#ifndef MS_WINDOWS +/* Get the blocking mode of the file descriptor. + Return 0 if the O_NONBLOCK flag is set, 1 if the flag is cleared, + raise an exception and return -1 on error. */ +int +_Py_get_blocking(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + + return !(flags & O_NONBLOCK); +} + +/* Set the blocking mode of the specified file descriptor. + + Set the O_NONBLOCK flag if blocking is False, clear the O_NONBLOCK flag + otherwise. + + Return 0 on success, raise an exception and return -1 on error. */ +int +_Py_set_blocking(int fd, int blocking) +{ +#if defined(HAVE_SYS_IOCTL_H) && defined(FIONBIO) + int arg = !blocking; + if (ioctl(fd, FIONBIO, &arg) < 0) + goto error; +#else + int flags, res; + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + goto error; + + if (blocking) + flags = flags & (~O_NONBLOCK); + else + flags = flags | O_NONBLOCK; + + res = fcntl(fd, F_SETFL, flags); + if (res < 0) + goto error; +#endif + return 0; + +error: + PyErr_SetFromErrno(PyExc_OSError); + return -1; +} +#endif +