diff --git a/radicale/server.py b/radicale/server.py index 22ee62f9..fd153cd7 100644 --- a/radicale/server.py +++ b/radicale/server.py @@ -22,6 +22,7 @@ Built-in WSGI server. """ +import errno import os import select import socket @@ -36,8 +37,8 @@ from radicale.log import logger if hasattr(socket, "EAI_ADDRFAMILY"): COMPAT_EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY -elif os.name == "nt" and hasattr(socket, "EAI_NONAME"): - # Windows doesn't have a special error code for this +elif hasattr(socket, "EAI_NONAME"): + # Windows and BSD don't have a special error code for this COMPAT_EAI_ADDRFAMILY = socket.EAI_NONAME if hasattr(socket, "IPPROTO_IPV6"): COMPAT_IPPROTO_IPV6 = socket.IPPROTO_IPV6 @@ -213,19 +214,26 @@ def serve(configuration, shutdown_socket): possible_families = (socket.AF_INET, socket.AF_INET6) bind_ok = False for i, family in enumerate(possible_families): + is_last = i == len(possible_families) - 1 try: server = server_class(configuration, family, address, RequestHandler) except OSError as e: - if ((bind_ok or i < len(possible_families) - 1) and - isinstance(e, socket.gaierror) and - e.errno in (socket.EAI_NONAME, - COMPAT_EAI_ADDRFAMILY)): - # Ignore unsupported families, only one must work + # Ignore unsupported families (only one must work) + if ((bind_ok or not is_last) and ( + isinstance(e, socket.gaierror) and ( + # Hostname does not exist or doesn't have + # address for address family + e.errno == socket.EAI_NONAME or + # Address not for address family + e.errno == COMPAT_EAI_ADDRFAMILY) or + # Workaround for PyPy + str(e) == "address family mismatched" or + # Address family not available (e.g. IPv6 disabled) + e.errno == errno.EADDRNOTAVAIL)): continue - raise RuntimeError( - "Failed to start server %r: %s" % ( - format_address(address), e)) from e + raise RuntimeError("Failed to start server %r: %s" % ( + format_address(address), e)) from e servers[server.socket] = server bind_ok = True server.set_app(application) diff --git a/radicale/tests/test_server.py b/radicale/tests/test_server.py index 51af416c..2c5aa071 100644 --- a/radicale/tests/test_server.py +++ b/radicale/tests/test_server.py @@ -19,6 +19,7 @@ Test the internal server. """ +import errno import os import shutil import socket @@ -116,22 +117,23 @@ class TestBaseServerRequests(BaseTest): self.get("/", check=302) def test_bind_fail(self): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - with pytest.raises(socket.gaierror) as exc_info: - sock.bind(("::1", 0)) - assert exc_info.value.errno == server.COMPAT_EAI_ADDRFAMILY - with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: - with pytest.raises(socket.gaierror) as exc_info: - sock.bind(("127.0.0.1", 0)) - assert exc_info.value.errno == server.COMPAT_EAI_ADDRFAMILY + for family, address in [(socket.AF_INET, "::1"), + (socket.AF_INET6, "127.0.0.1")]: + with socket.socket(family, socket.SOCK_STREAM) as sock: + with pytest.raises(OSError) as exc_info: + sock.bind((address, 0)) + assert (isinstance(exc_info.value, socket.gaierror) and + exc_info.value.errno == server.COMPAT_EAI_ADDRFAMILY or + # Workaround for PyPy + str(exc_info.value) == "address family mismatched") def test_ipv6(self): with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: try: # Find available port sock.bind(("::1", 0)) - except socket.gaierror as e: - if e.errno == server.COMPAT_EAI_ADDRFAMILY: + except OSError as e: + if e.errno == errno.EADDRNOTAVAIL: pytest.skip("IPv6 not supported") raise self.sockname = sock.getsockname()[:2]