diff --git a/Lib/socket.py b/Lib/socket.py index 9ac0badc660..2a4f875f76b 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -56,6 +56,7 @@ import os import sys from enum import IntEnum, IntFlag +from functools import partial try: import errno @@ -122,7 +123,7 @@ def _intenum_converter(value, enum_klass): 10004: "The operation was interrupted.", 10009: "A bad file handle was passed.", 10013: "Permission denied.", - 10014: "A fault occurred on the network??", + 10014: "An invalid pointer was passed.", 10022: "An invalid operation was attempted.", 10024: "Too many open files.", 10035: "The socket operation would block.", @@ -180,7 +181,7 @@ def _intenum_converter(value, enum_klass): 11001: "Host not found.", 11002: "Nonauthoritative host not found.", 11003: "This is a nonrecoverable error.", - 11004: "Valid name, no data record requested type.", + 11004: "Valid name, no data record of requested type.", 11005: "QoS receivers.", 11006: "QoS senders.", 11007: "No QoS senders.", @@ -196,7 +197,7 @@ def _intenum_converter(value, enum_klass): 11017: "QoS flowspec error.", 11018: "Invalid QoS provider buffer.", 11019: "Invalid QoS filter style.", - 11020: "Invalid QoS filter style.", + 11020: "Invalid QoS filter type.", 11021: "Incorrect QoS filter count.", 11022: "Invalid QoS object length.", 11023: "Incorrect QoS flow count.", @@ -348,75 +349,83 @@ def makefile(self, mode="r", buffering=None, *, text.mode = mode return text - if hasattr(os, 'sendfile'): + def _sendfile_zerocopy(self, zerocopy_func, giveup_exc_type, file, + offset=0, count=None): + """ + Send a file using a zero-copy function. + """ + import selectors - def _sendfile_use_sendfile(self, file, offset=0, count=None): - # Lazy import to improve module import time - import selectors + self._check_sendfile_params(file, offset, count) + sockno = self.fileno() + try: + fileno = file.fileno() + except (AttributeError, io.UnsupportedOperation) as err: + raise giveup_exc_type(err) # not a regular file + try: + fsize = os.fstat(fileno).st_size + except OSError as err: + raise giveup_exc_type(err) # not a regular file + if not fsize: + return 0 # empty file + # Truncate to 1GiB to avoid OverflowError, see bpo-38319. + blocksize = min(count or fsize, 2 ** 30) + timeout = self.gettimeout() + if timeout == 0: + raise ValueError("non-blocking sockets are not supported") + # poll/select have the advantage of not requiring any + # extra file descriptor, contrarily to epoll/kqueue + # (also, they require a single syscall). + if hasattr(selectors, 'PollSelector'): + selector = selectors.PollSelector() + else: + selector = selectors.SelectSelector() + selector.register(sockno, selectors.EVENT_WRITE) - self._check_sendfile_params(file, offset, count) - sockno = self.fileno() - try: - fileno = file.fileno() - except (AttributeError, io.UnsupportedOperation) as err: - raise _GiveupOnSendfile(err) # not a regular file - try: - fsize = os.fstat(fileno).st_size - except OSError as err: - raise _GiveupOnSendfile(err) # not a regular file - if not fsize: - return 0 # empty file - # Truncate to 1GiB to avoid OverflowError, see bpo-38319. - blocksize = min(count or fsize, 2 ** 30) - timeout = self.gettimeout() - if timeout == 0: - raise ValueError("non-blocking sockets are not supported") - # poll/select have the advantage of not requiring any - # extra file descriptor, contrarily to epoll/kqueue - # (also, they require a single syscall). - if hasattr(selectors, 'PollSelector'): - selector = selectors.PollSelector() - else: - selector = selectors.SelectSelector() - selector.register(sockno, selectors.EVENT_WRITE) - - total_sent = 0 - # localize variable access to minimize overhead - selector_select = selector.select - os_sendfile = os.sendfile - try: - while True: - if timeout and not selector_select(timeout): - raise TimeoutError('timed out') - if count: - blocksize = min(count - total_sent, blocksize) - if blocksize <= 0: - break - try: - sent = os_sendfile(sockno, fileno, offset, blocksize) - except BlockingIOError: - if not timeout: - # Block until the socket is ready to send some - # data; avoids hogging CPU resources. - selector_select() - continue - except OSError as err: - if total_sent == 0: - # We can get here for different reasons, the main - # one being 'file' is not a regular mmap(2)-like - # file, in which case we'll fall back on using - # plain send(). - raise _GiveupOnSendfile(err) - raise err from None - else: - if sent == 0: - break # EOF - offset += sent - total_sent += sent - return total_sent - finally: - if total_sent > 0 and hasattr(file, 'seek'): - file.seek(offset) + total_sent = 0 + # localize variable access to minimize overhead + selector_select = selector.select + try: + while True: + if timeout and not selector_select(timeout): + raise TimeoutError('timed out') + if count: + blocksize = min(count - total_sent, blocksize) + if blocksize <= 0: + break + try: + sent = zerocopy_func(fileno, offset, blocksize) + except BlockingIOError: + if not timeout: + # Block until the socket is ready to send some + # data; avoids hogging CPU resources. + selector_select() + continue + except OSError as err: + if total_sent == 0: + # We can get here for different reasons, the main + # one being 'file' is not a regular mmap(2)-like + # file, in which case we'll fall back on using + # plain send(). + raise giveup_exc_type(err) + raise err from None + else: + if sent == 0: + break # EOF + offset += sent + total_sent += sent + return total_sent + finally: + if total_sent > 0 and hasattr(file, 'seek'): + file.seek(offset) + + if hasattr(os, 'sendfile'): + def _sendfile_use_sendfile(self, file, offset=0, count=None): + return self._sendfile_zerocopy( + partial(os.sendfile, self.fileno()), + _GiveupOnSendfile, + file, offset, count, + ) else: def _sendfile_use_sendfile(self, file, offset=0, count=None): raise _GiveupOnSendfile( diff --git a/Lib/socketserver.py b/Lib/socketserver.py index 35b2723de3b..ec389457ef5 100644 --- a/Lib/socketserver.py +++ b/Lib/socketserver.py @@ -120,9 +120,6 @@ class will essentially render the service "deaf" while one request is # Author of the BaseServer patch: Luke Kenneth Casson Leighton -__version__ = "0.4" - - import socket import selectors import os @@ -441,7 +438,7 @@ class TCPServer(BaseServer): socket_type = socket.SOCK_STREAM - request_queue_size = 5 + request_queue_size = getattr(socket, "SOMAXCONN", 5) allow_reuse_address = False @@ -861,3 +858,12 @@ def setup(self): def finish(self): self.socket.sendto(self.wfile.getvalue(), self.client_address) + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "0.4" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 0e20723b6f3..6a20501b046 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -8,7 +8,9 @@ import _thread as thread import array import contextlib +import decimal import errno +import fractions import gc import io import itertools @@ -1349,10 +1351,20 @@ def testDefaultTimeout(self): self.assertEqual(s.gettimeout(), None) # Set the default timeout to 10, and see if it propagates - with socket_setdefaulttimeout(10): - self.assertEqual(socket.getdefaulttimeout(), 10) + with socket_setdefaulttimeout(10.125): + self.assertEqual(socket.getdefaulttimeout(), 10.125) with socket.socket() as sock: - self.assertEqual(sock.gettimeout(), 10) + self.assertEqual(sock.gettimeout(), 10.125) + + socket.setdefaulttimeout(decimal.Decimal('11.125')) + self.assertEqual(socket.getdefaulttimeout(), 11.125) + with socket.socket() as sock: + self.assertEqual(sock.gettimeout(), 11.125) + + socket.setdefaulttimeout(fractions.Fraction(97, 8)) + self.assertEqual(socket.getdefaulttimeout(), 12.125) + with socket.socket() as sock: + self.assertEqual(sock.gettimeout(), 12.125) # Reset the default timeout to None, and see if it propagates socket.setdefaulttimeout(None) @@ -1580,6 +1592,40 @@ def testSetSockOpt(self): reuse = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) self.assertFalse(reuse == 0, "failed to set reuse mode") + def test_setsockopt_errors(self): + # See issue #107546. + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.addCleanup(sock.close) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # No error expected. + + with self.assertRaises(OverflowError): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 2 ** 100) + + with self.assertRaises(OverflowError): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, - 2 ** 100) + + with self.assertRaises(OverflowError): + sock.setsockopt(socket.SOL_SOCKET, 2 ** 100, 1) + + with self.assertRaises(OverflowError): + sock.setsockopt(2 ** 100, socket.SO_REUSEADDR, 1) + + with self.assertRaisesRegex(TypeError, "socket option should be int, bytes-like object or None"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, dict()) + + with self.assertRaisesRegex(TypeError, "requires 4 arguments when the third argument is None"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, None) + + with self.assertRaisesRegex(TypeError, "only takes 4 arguments when the third argument is None"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1, 2) + + with self.assertRaisesRegex(TypeError, "takes at least 3 arguments"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) + + with self.assertRaisesRegex(TypeError, "takes at most 4 arguments"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1, 2, 3) + def testSendAfterClose(self): # testing send() after close() with timeout with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: @@ -2435,6 +2481,45 @@ def testCrucialConstants(self): socket.CAN_ISOTP socket.SOCK_DGRAM + @unittest.skipUnless(hasattr(socket, "SOL_CAN_ISOTP"), + "missing ") + def testISOTP(self): + socket.SOL_CAN_ISOTP + + socket.CAN_ISOTP_OPTS + socket.CAN_ISOTP_RECV_FC + + socket.CAN_ISOTP_TX_STMIN + socket.CAN_ISOTP_RX_STMIN + socket.CAN_ISOTP_LL_OPTS + + socket.CAN_ISOTP_LISTEN_MODE + socket.CAN_ISOTP_EXTEND_ADDR + socket.CAN_ISOTP_TX_PADDING + socket.CAN_ISOTP_RX_PADDING + socket.CAN_ISOTP_CHK_PAD_LEN + socket.CAN_ISOTP_CHK_PAD_DATA + socket.CAN_ISOTP_HALF_DUPLEX + socket.CAN_ISOTP_FORCE_TXSTMIN + socket.CAN_ISOTP_FORCE_RXSTMIN + socket.CAN_ISOTP_RX_EXT_ADDR + socket.CAN_ISOTP_WAIT_TX_DONE + # This constant is not always available + # socket.CAN_ISOTP_SF_BROADCAST + + socket.CAN_ISOTP_DEFAULT_FLAGS + socket.CAN_ISOTP_DEFAULT_EXT_ADDRESS + socket.CAN_ISOTP_DEFAULT_PAD_CONTENT + socket.CAN_ISOTP_DEFAULT_FRAME_TXTIME + socket.CAN_ISOTP_DEFAULT_RECV_BS + socket.CAN_ISOTP_DEFAULT_EXT_ADDRESS + socket.CAN_ISOTP_DEFAULT_RECV_STMIN + socket.CAN_ISOTP_DEFAULT_RECV_WFTMAX + + socket.CAN_ISOTP_DEFAULT_LL_MTU + socket.CAN_ISOTP_DEFAULT_LL_TX_DL + socket.CAN_ISOTP_DEFAULT_LL_TX_FLAGS + def testCreateSocket(self): with socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW) as s: pass diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py index 2ca356606b2..ca33a9a6dac 100644 --- a/Lib/test/test_socketserver.py +++ b/Lib/test/test_socketserver.py @@ -17,6 +17,7 @@ from test.support import os_helper from test.support import socket_helper from test.support import threading_helper +from test.support import warnings_helper test.support.requires("network") @@ -43,6 +44,7 @@ def receive(sock, n, timeout=test.support.SHORT_TIMEOUT): raise RuntimeError("timed out on %r" % (sock,)) +@warnings_helper.ignore_fork_in_thread_deprecation_warnings() @test.support.requires_fork() @contextlib.contextmanager def simple_subprocess(testcase): @@ -173,6 +175,7 @@ def test_ThreadingTCPServer(self): socketserver.StreamRequestHandler, self.stream_examine) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_forking def test_ForkingTCPServer(self): with simple_subprocess(self): @@ -192,6 +195,7 @@ def test_ThreadingUnixStreamServer(self): socketserver.StreamRequestHandler, self.stream_examine) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_unix_sockets @requires_forking def test_ForkingUnixStreamServer(self): @@ -210,6 +214,7 @@ def test_ThreadingUDPServer(self): socketserver.DatagramRequestHandler, self.dgram_examine) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_forking def test_ForkingUDPServer(self): with simple_subprocess(self): @@ -233,6 +238,7 @@ def test_ThreadingUnixDatagramServer(self): socketserver.DatagramRequestHandler, self.dgram_examine) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_unix_sockets @requires_forking def test_ForkingUnixDatagramServer(self): @@ -318,11 +324,13 @@ def test_threading_not_handled(self): self.assertIs(cm.exc_type, SystemExit) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_forking def test_forking_handled(self): ForkingErrorTestServer(ValueError) self.check_result(handled=True) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_forking def test_forking_not_handled(self): ForkingErrorTestServer(SystemExit) @@ -507,5 +515,15 @@ class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): server.server_close() +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(socketserver, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 9635cb77315..db30bc138ac 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -21,7 +21,7 @@ mod _socket { }, convert::{IntoPyException, ToPyObject, TryFromBorrowedObject, TryFromObject}, function::{ - ArgBytesLike, ArgIntoFloat, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath, + ArgBytesLike, ArgIndex, ArgIntoFloat, ArgMemoryBuffer, ArgStrOrBytesLike, FsPath, FuncArgs, OptionalArg, }, types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable}, @@ -2441,29 +2441,115 @@ mod _socket { #[pymethod] fn setsockopt( zelf: &Py, - level: i32, - name: i32, - value: Option>, - optlen: OptionalArg, + args: FuncArgs, vm: &VirtualMachine, ) -> Result<(), IoOrPyException> { - let sock = zelf.sock()?; - let fd = sock_fileno(&sock); - match (value, optlen) { - (Some(Either::A(b)), OptionalArg::Missing) => { - b.with_ref(|b| host_socket::setsockopt_bytes(fd as _, level, name, b))? + if !args.kwargs.is_empty() { + return Err(vm + .new_type_error("setsockopt() takes no keyword arguments") + .into()); + } + let nargs = args.args.len(); + if nargs < 3 { + return Err(vm + .new_type_error(format!( + "setsockopt() takes at least 3 arguments ({nargs} given)" + )) + .into()); + } + if nargs > 4 { + return Err(vm + .new_type_error(format!( + "setsockopt() takes at most 4 arguments ({nargs} given)" + )) + .into()); + } + let (level, name, value, optlen): ( + i32, + i32, + Option, + OptionalArg, + ) = args.bind(vm)?; + let optlen = match optlen { + OptionalArg::Present(index) => { + let index = index.as_ref(); + // CPython's unsigned-int bitfield conversion masks the value, + // warning only when it fits neither a signed nor unsigned int. + if !index.as_bigint().to_i64().is_some_and(|value| { + (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value) + }) { + crate::vm::warn::warn( + vm.ctx.new_str("integer value out of range").into(), + Some(vm.ctx.exceptions.deprecation_warning.to_owned()), + 1, + None, + vm, + )?; + } + OptionalArg::Present(index.as_u32_mask()) } - (Some(Either::B(val)), OptionalArg::Missing) => { - host_socket::setsockopt_int(fd as _, level, name, val)? + OptionalArg::Missing => OptionalArg::Missing, + }; + enum Value { + Int(i32), + Buffer(ArgBytesLike), + Null(u32), + } + let value = match (value, optlen) { + (Some(value), OptionalArg::Missing) => { + if let Some(index) = value.try_index_opt(vm) { + Value::Int(index?.try_to_primitive::(vm)?) + } else if value.check_buffer() { + let buffer = ArgBytesLike::try_from_object(vm, value)?; + #[cfg(windows)] + if buffer.len() > i32::MAX as usize { + return Err(vm + .new_overflow_error(format!( + "socket option is larger than {} bytes", + i32::MAX + )) + .into()); + } + Value::Buffer(buffer) + } else { + return Err(vm + .new_type_error(format!( + "socket option should be int, bytes-like object or None (got {})", + value.class().fully_qualified_name(vm)? + )) + .into()); + } } - (None, OptionalArg::Present(optlen)) => { - host_socket::setsockopt_none(fd as _, level, name, optlen)? + (None, OptionalArg::Present(optlen)) => Value::Null(optlen), + (None, OptionalArg::Missing) => { + return Err(vm + .new_type_error( + "setsockopt() requires 4 arguments when the third argument is None", + ) + .into()); } - _ => { + (Some(value), OptionalArg::Present(_)) => { return Err(vm - .new_type_error("expected the value arg xor the optlen arg") + .new_type_error(format!( + "setsockopt() only takes 4 arguments when the third argument is None (got {})", + value.class().fully_qualified_name(vm)? + )) .into()); } + }; + // Convert Python values before locking the socket; callbacks may close it. + let sock = zelf.sock()?; + let fd = sock_fileno(&sock); + match &value { + Value::Int(value) => { + host_socket::setsockopt_int(fd as _, level, name, *value)?; + } + Value::Buffer(buffer) => { + buffer.with_ref(|b| host_socket::setsockopt_bytes(fd as _, level, name, b))?; + } + Value::Null(optlen) => { + host_socket::setsockopt_none(fd as _, level, name, *optlen)?; + } } Ok(()) } @@ -2962,7 +3048,7 @@ mod _socket { #[pyarg(any)] host: Option, #[pyarg(any)] - port: Option>, + port: Option, #[pyarg(any, default = c::AF_UNSPEC)] family: i32, @@ -3014,10 +3100,23 @@ mod _socket { }; let host = host_encoded.as_deref(); - // Encode port: str/bytes as service name, int as port number - let port_encoded: Option = match opts.port.as_ref() { - Some(Either::A(sb)) => { - let port_str = match sb { + // Check the index protocol before string/bytes conversion, including for + // subclasses, and preserve any exception raised by __index__. + let port_index = opts + .port + .as_ref() + .and_then(|port| port.try_index_opt(vm)) + .transpose()?; + let port_encoded: Option = match (port_index, opts.port.as_ref()) { + (Some(index), _) => Some(crate::vm::builtins::PyInt::repr_str(&index, vm)?), + (None, Some(port)) => { + if !port.fast_isinstance(vm.ctx.types.str_type) + && !port.fast_isinstance(vm.ctx.types.bytes_type) + { + return Err(vm.new_os_error("Int or String expected").into()); + } + let sb = ArgStrOrBytesLike::try_from_object(vm, port.clone())?; + let port_str = match &sb { ArgStrOrBytesLike::Str(s) => { // For str, check for surrogates and raise UnicodeEncodeError if found s.to_str() @@ -3055,8 +3154,7 @@ mod _socket { }; Some(port_str) } - Some(Either::B(i)) => Some(i.to_string()), - None => None, + (None, None) => None, }; let port = port_encoded.as_deref(); @@ -3070,7 +3168,7 @@ mod _socket { ai.address, ai.socktype, ai.protocol, - ai.canonname, + ai.canonname.unwrap_or_default(), get_ip_addr_tuple(&ai.sockaddr, vm), )) .into()