From 8cc2999168fdf2ade3d3170f1d848db5b4234c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Mon, 2 Nov 2026 19:45:40 -0500 Subject: [PATCH 1/2] Honour socket timeout in connect and connect_ex --- src/core/IronPython.Modules/_socket.cs | 35 ++++++++---- .../modules/network_related/test__socket.py | 53 +++++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/core/IronPython.Modules/_socket.cs b/src/core/IronPython.Modules/_socket.cs index 863346278..846a06ed3 100644 --- a/src/core/IronPython.Modules/_socket.cs +++ b/src/core/IronPython.Modules/_socket.cs @@ -278,18 +278,18 @@ private void _close() { + "\n" + "Raises socket.error if the socket has been closed, the socket is listening, or\n" + "another connection error occurred." - + "\n" - + "Difference from CPython: connect() does not support timeouts in blocking mode.\n" - + "If a timeout is set and the socket is in blocking mode, connect() will block\n" - + "indefinitely until a connection is made or an error occurs." )] public void connect([NotNone] PythonTuple address) { IPEndPoint remoteEP = TupleToEndPoint(_context, address, _socket.AddressFamily, out _hostName); + bool connected; try { - _socket.Connect(remoteEP); + connected = TryConnect(remoteEP); } catch (Exception e) { throw MakeException(_context, e); } + if (!connected) { + throw PythonExceptions.CreateThrowable(timeout(_context), "timed out"); + } } [Documentation("connect_ex(address) -> error_code\n\n" @@ -299,21 +299,36 @@ public void connect([NotNone] PythonTuple address) { + "will still be raised.\n" + "\n" + "A return value of 0 indicates that the connect call was successful." - + "\n" - + "Difference from CPython: connect_ex() does not support timeouts in blocking\n" - + "mode. If a timeout is set and the socket is in blocking mode, connect_ex() will\n" - + "block indefinitely until a connection is made or an error occurs." )] public int connect_ex([NotNone] PythonTuple address) { IPEndPoint remoteEP = TupleToEndPoint(_context, address, _socket.AddressFamily, out _hostName); try { - _socket.Connect(remoteEP); + if (!TryConnect(remoteEP)) { + return PythonErrno.EWOULDBLOCK; + } } catch (SocketException ex) { return !ClrModule.IsMono ? ex.NativeErrorCode : MapMonoSocketErrorToErrno(ex.SocketErrorCode); } return PythonErrno.ENOERROR; } + /// + /// Returns false if the timeout elapsed. The attempt is then left pending, as in CPython. + /// + private bool TryConnect(IPEndPoint remoteEP) { + if (_timeout == 0) { + _socket.Connect(remoteEP); + return true; + } + + IAsyncResult result = _socket.BeginConnect(remoteEP, null, null); + if (!result.AsyncWaitHandle.WaitOne(_timeout)) { + return false; + } + _socket.EndConnect(result); + return true; + } + public long detach() { var fd = fileno(); _socket = null!; // TODO: this is wrong, no null checks on _socket anywhere! diff --git a/tests/suite/modules/network_related/test__socket.py b/tests/suite/modules/network_related/test__socket.py index 428643f0f..871112544 100644 --- a/tests/suite/modules/network_related/test__socket.py +++ b/tests/suite/modules/network_related/test__socket.py @@ -6,6 +6,7 @@ # test _socket # +import errno import os import _socket import sys @@ -692,4 +693,56 @@ def server_thread(): f.close() +class SocketTimeoutTest(IronPythonTestCase): + BLACKHOLE = ('10.255.255.1', 22) # an unroutable address + + def test_create_connection_timeout(self): + start = time.time() + try: + socket.create_connection(self.BLACKHOLE, timeout=1).close() + except socket.timeout: + pass + except OSError as e: + self.skipTest("create_connection failed without timing out: %s" % e) + else: + self.skipTest("create_connection succeeded") + self.assertLess(time.time() - start, 5) + + def test_connect_timeout(self): + with socket.socket() as s: + s.settimeout(1) + start = time.time() + try: + s.connect(self.BLACKHOLE) + except socket.timeout: + pass + except OSError as e: + self.skipTest("connect failed without timing out: %s" % e) + else: + self.skipTest("connect succeeded") + self.assertLess(time.time() - start, 5) + self.assertEqual(s.gettimeout(), 1) + + def test_connect_ex_timeout(self): + with socket.socket() as s: + s.settimeout(1) + start = time.time() + err = s.connect_ex(self.BLACKHOLE) + self.assertLess(time.time() - start, 5) + if err != errno.EWOULDBLOCK: + self.skipTest("connect_ex returned %d instead of timing out" % err) + + def test_connect_within_timeout(self): + # make sure we still connect when timeout is set + with socket.socket() as server: + server.bind(('127.0.0.1', 0)) + server.listen(1) + with socket.socket() as s: + s.settimeout(5) + s.connect(server.getsockname()) + conn, _ = server.accept() + with conn: + s.sendall(b'abc') + self.assertEqual(conn.recv(3), b'abc') + run_test(__name__) From fc7e9867627d7c21d483e77934d3ef13ee78e686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Mon, 2 Nov 2026 23:13:31 -0500 Subject: [PATCH 2/2] Skip flaky test --- tests/suite/stdlib/test_support.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/suite/stdlib/test_support.py b/tests/suite/stdlib/test_support.py index d38a38be6..87865de38 100644 --- a/tests/suite/stdlib/test_support.py +++ b/tests/suite/stdlib/test_support.py @@ -24,6 +24,10 @@ def load_tests(loader, standard_tests, pattern): ] skip_tests = [] + if sys.version_info >= (3, 6): + skip_tests += [ + test.test_support.TestSupport('test_fd_count'), # flaky: a finalizer can close a leaked fd between the two counts + ] return generate_suite(tests, failing_tests, skip_tests)