Skip to content

Commit ab556e2

Browse files
committed
Add test/support/socket_helper from CPython 3.10
1 parent db78893 commit ab556e2

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

Lib/test/support/socket_helper.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import contextlib
2+
import errno
3+
import socket
4+
import unittest
5+
import sys
6+
7+
from .. import support
8+
9+
10+
HOST = "localhost"
11+
HOSTv4 = "127.0.0.1"
12+
HOSTv6 = "::1"
13+
14+
15+
def find_unused_port(family=socket.AF_INET, socktype=socket.SOCK_STREAM):
16+
"""Returns an unused port that should be suitable for binding. This is
17+
achieved by creating a temporary socket with the same family and type as
18+
the 'sock' parameter (default is AF_INET, SOCK_STREAM), and binding it to
19+
the specified host address (defaults to 0.0.0.0) with the port set to 0,
20+
eliciting an unused ephemeral port from the OS. The temporary socket is
21+
then closed and deleted, and the ephemeral port is returned.
22+
23+
Either this method or bind_port() should be used for any tests where a
24+
server socket needs to be bound to a particular port for the duration of
25+
the test. Which one to use depends on whether the calling code is creating
26+
a python socket, or if an unused port needs to be provided in a constructor
27+
or passed to an external program (i.e. the -accept argument to openssl's
28+
s_server mode). Always prefer bind_port() over find_unused_port() where
29+
possible. Hard coded ports should *NEVER* be used. As soon as a server
30+
socket is bound to a hard coded port, the ability to run multiple instances
31+
of the test simultaneously on the same host is compromised, which makes the
32+
test a ticking time bomb in a buildbot environment. On Unix buildbots, this
33+
may simply manifest as a failed test, which can be recovered from without
34+
intervention in most cases, but on Windows, the entire python process can
35+
completely and utterly wedge, requiring someone to log in to the buildbot
36+
and manually kill the affected process.
37+
38+
(This is easy to reproduce on Windows, unfortunately, and can be traced to
39+
the SO_REUSEADDR socket option having different semantics on Windows versus
40+
Unix/Linux. On Unix, you can't have two AF_INET SOCK_STREAM sockets bind,
41+
listen and then accept connections on identical host/ports. An EADDRINUSE
42+
OSError will be raised at some point (depending on the platform and
43+
the order bind and listen were called on each socket).
44+
45+
However, on Windows, if SO_REUSEADDR is set on the sockets, no EADDRINUSE
46+
will ever be raised when attempting to bind two identical host/ports. When
47+
accept() is called on each socket, the second caller's process will steal
48+
the port from the first caller, leaving them both in an awkwardly wedged
49+
state where they'll no longer respond to any signals or graceful kills, and
50+
must be forcibly killed via OpenProcess()/TerminateProcess().
51+
52+
The solution on Windows is to use the SO_EXCLUSIVEADDRUSE socket option
53+
instead of SO_REUSEADDR, which effectively affords the same semantics as
54+
SO_REUSEADDR on Unix. Given the propensity of Unix developers in the Open
55+
Source world compared to Windows ones, this is a common mistake. A quick
56+
look over OpenSSL's 0.9.8g source shows that they use SO_REUSEADDR when
57+
openssl.exe is called with the 's_server' option, for example. See
58+
http://bugs.python.org/issue2550 for more info. The following site also
59+
has a very thorough description about the implications of both REUSEADDR
60+
and EXCLUSIVEADDRUSE on Windows:
61+
http://msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx)
62+
63+
XXX: although this approach is a vast improvement on previous attempts to
64+
elicit unused ports, it rests heavily on the assumption that the ephemeral
65+
port returned to us by the OS won't immediately be dished back out to some
66+
other process when we close and delete our temporary socket but before our
67+
calling code has a chance to bind the returned port. We can deal with this
68+
issue if/when we come across it.
69+
"""
70+
71+
with socket.socket(family, socktype) as tempsock:
72+
port = bind_port(tempsock)
73+
del tempsock
74+
return port
75+
76+
def bind_port(sock, host=HOST):
77+
"""Bind the socket to a free port and return the port number. Relies on
78+
ephemeral ports in order to ensure we are using an unbound port. This is
79+
important as many tests may be running simultaneously, especially in a
80+
buildbot environment. This method raises an exception if the sock.family
81+
is AF_INET and sock.type is SOCK_STREAM, *and* the socket has SO_REUSEADDR
82+
or SO_REUSEPORT set on it. Tests should *never* set these socket options
83+
for TCP/IP sockets. The only case for setting these options is testing
84+
multicasting via multiple UDP sockets.
85+
86+
Additionally, if the SO_EXCLUSIVEADDRUSE socket option is available (i.e.
87+
on Windows), it will be set on the socket. This will prevent anyone else
88+
from bind()'ing to our host/port for the duration of the test.
89+
"""
90+
91+
if sock.family == socket.AF_INET and sock.type == socket.SOCK_STREAM:
92+
if hasattr(socket, 'SO_REUSEADDR'):
93+
if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1:
94+
raise support.TestFailed("tests should never set the "
95+
"SO_REUSEADDR socket option on "
96+
"TCP/IP sockets!")
97+
if hasattr(socket, 'SO_REUSEPORT'):
98+
try:
99+
if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1:
100+
raise support.TestFailed("tests should never set the "
101+
"SO_REUSEPORT socket option on "
102+
"TCP/IP sockets!")
103+
except OSError:
104+
# Python's socket module was compiled using modern headers
105+
# thus defining SO_REUSEPORT but this process is running
106+
# under an older kernel that does not support SO_REUSEPORT.
107+
pass
108+
if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
109+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
110+
111+
sock.bind((host, 0))
112+
port = sock.getsockname()[1]
113+
return port
114+
115+
def bind_unix_socket(sock, addr):
116+
"""Bind a unix socket, raising SkipTest if PermissionError is raised."""
117+
assert sock.family == socket.AF_UNIX
118+
try:
119+
sock.bind(addr)
120+
except PermissionError:
121+
sock.close()
122+
raise unittest.SkipTest('cannot bind AF_UNIX sockets')
123+
124+
def _is_ipv6_enabled():
125+
"""Check whether IPv6 is enabled on this host."""
126+
if socket.has_ipv6:
127+
sock = None
128+
try:
129+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
130+
sock.bind((HOSTv6, 0))
131+
return True
132+
except OSError:
133+
pass
134+
finally:
135+
if sock:
136+
sock.close()
137+
return False
138+
139+
IPV6_ENABLED = _is_ipv6_enabled()
140+
141+
142+
_bind_nix_socket_error = None
143+
def skip_unless_bind_unix_socket(test):
144+
"""Decorator for tests requiring a functional bind() for unix sockets."""
145+
if not hasattr(socket, 'AF_UNIX'):
146+
return unittest.skip('No UNIX Sockets')(test)
147+
global _bind_nix_socket_error
148+
if _bind_nix_socket_error is None:
149+
from .os_helper import TESTFN, unlink
150+
path = TESTFN + "can_bind_unix_socket"
151+
with socket.socket(socket.AF_UNIX) as sock:
152+
try:
153+
sock.bind(path)
154+
_bind_nix_socket_error = False
155+
except OSError as e:
156+
_bind_nix_socket_error = e
157+
finally:
158+
unlink(path)
159+
if _bind_nix_socket_error:
160+
msg = 'Requires a functional unix bind(): %s' % _bind_nix_socket_error
161+
return unittest.skip(msg)(test)
162+
else:
163+
return test
164+
165+
166+
def get_socket_conn_refused_errs():
167+
"""
168+
Get the different socket error numbers ('errno') which can be received
169+
when a connection is refused.
170+
"""
171+
errors = [errno.ECONNREFUSED]
172+
if hasattr(errno, 'ENETUNREACH'):
173+
# On Solaris, ENETUNREACH is returned sometimes instead of ECONNREFUSED
174+
errors.append(errno.ENETUNREACH)
175+
if hasattr(errno, 'EADDRNOTAVAIL'):
176+
# bpo-31910: socket.create_connection() fails randomly
177+
# with EADDRNOTAVAIL on Travis CI
178+
errors.append(errno.EADDRNOTAVAIL)
179+
if hasattr(errno, 'EHOSTUNREACH'):
180+
# bpo-37583: The destination host cannot be reached
181+
errors.append(errno.EHOSTUNREACH)
182+
if not IPV6_ENABLED:
183+
errors.append(errno.EAFNOSUPPORT)
184+
return errors
185+
186+
187+
_NOT_SET = object()
188+
189+
@contextlib.contextmanager
190+
def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()):
191+
"""Return a context manager that raises ResourceDenied when various issues
192+
with the internet connection manifest themselves as exceptions."""
193+
import nntplib
194+
import urllib.error
195+
if timeout is _NOT_SET:
196+
timeout = support.INTERNET_TIMEOUT
197+
198+
default_errnos = [
199+
('ECONNREFUSED', 111),
200+
('ECONNRESET', 104),
201+
('EHOSTUNREACH', 113),
202+
('ENETUNREACH', 101),
203+
('ETIMEDOUT', 110),
204+
# socket.create_connection() fails randomly with
205+
# EADDRNOTAVAIL on Travis CI.
206+
('EADDRNOTAVAIL', 99),
207+
]
208+
default_gai_errnos = [
209+
('EAI_AGAIN', -3),
210+
('EAI_FAIL', -4),
211+
('EAI_NONAME', -2),
212+
('EAI_NODATA', -5),
213+
# Encountered when trying to resolve IPv6-only hostnames
214+
('WSANO_DATA', 11004),
215+
]
216+
217+
denied = support.ResourceDenied("Resource %r is not available" % resource_name)
218+
captured_errnos = errnos
219+
gai_errnos = []
220+
if not captured_errnos:
221+
captured_errnos = [getattr(errno, name, num)
222+
for (name, num) in default_errnos]
223+
gai_errnos = [getattr(socket, name, num)
224+
for (name, num) in default_gai_errnos]
225+
226+
def filter_error(err):
227+
n = getattr(err, 'errno', None)
228+
if (isinstance(err, TimeoutError) or
229+
(isinstance(err, socket.gaierror) and n in gai_errnos) or
230+
(isinstance(err, urllib.error.HTTPError) and
231+
500 <= err.code <= 599) or
232+
(isinstance(err, urllib.error.URLError) and
233+
(("ConnectionRefusedError" in err.reason) or
234+
("TimeoutError" in err.reason) or
235+
("EOFError" in err.reason))) or
236+
n in captured_errnos):
237+
if not support.verbose:
238+
sys.stderr.write(denied.args[0] + "\n")
239+
raise denied from err
240+
241+
old_timeout = socket.getdefaulttimeout()
242+
try:
243+
if timeout is not None:
244+
socket.setdefaulttimeout(timeout)
245+
yield
246+
except nntplib.NNTPTemporaryError as err:
247+
if support.verbose:
248+
sys.stderr.write(denied.args[0] + "\n")
249+
raise denied from err
250+
except OSError as err:
251+
# urllib can wrap original socket errors multiple times (!), we must
252+
# unwrap to get at the original error.
253+
while True:
254+
a = err.args
255+
if len(a) >= 1 and isinstance(a[0], OSError):
256+
err = a[0]
257+
# The error can also be wrapped as args[1]:
258+
# except socket.error as msg:
259+
# raise OSError('socket error', msg).with_traceback(sys.exc_info()[2])
260+
elif len(a) >= 2 and isinstance(a[1], OSError):
261+
err = a[1]
262+
else:
263+
break
264+
filter_error(err)
265+
raise
266+
# XXX should we catch generic exceptions and look for their
267+
# __cause__ or __context__?
268+
finally:
269+
socket.setdefaulttimeout(old_timeout)

0 commit comments

Comments
 (0)