]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-73458: Fix logging.config.listen() on a host without an IPv4 address (GH...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 24 Jul 2026 15:28:11 +0000 (17:28 +0200)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 15:28:11 +0000 (15:28 +0000)
The server is created in a thread which set the "ready" event only after a
successful start, so a failure to start left the caller waiting for that
event forever.  Set it also on failure.

The receiver always used AF_INET, which fails if the host has no IPv4
address, for example if "localhost" is only aliased to ::1.  Use the family
of the first resolved address in such case.

Also fixes gh-82076.
(cherry picked from commit 45a10f5d711ebe1bce9c6b93c10bbc5fe5bdf392)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lib/logging/config.py
Lib/test/test_logging.py
Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst [new file with mode: 0644]

index 3d9aa00fa52d1166b3f6fe0ad8fb9511bc3e1057..e0524eb5e3d89c25e820bc69ad74109cf148c74a 100644 (file)
@@ -32,6 +32,7 @@ import logging.handlers
 import os
 import queue
 import re
+import socket
 import struct
 import threading
 import traceback
@@ -1022,6 +1023,15 @@ def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None):
 
         def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
                      handler=None, ready=None, verify=None):
+            # The host can have no IPv4 address, for example if "localhost"
+            # is only aliased to ::1.  Leave resolution errors to the server.
+            try:
+                infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
+            except OSError:
+                pass
+            else:
+                if not any(info[0] == socket.AF_INET for info in infos):
+                    self.address_family = infos[0][0]
             ThreadingTCPServer.__init__(self, (host, port), handler)
             with logging._lock:
                 self.abort = 0
@@ -1053,9 +1063,14 @@ def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None):
             self.ready = threading.Event()
 
         def run(self):
-            server = self.rcvr(port=self.port, handler=self.hdlr,
-                               ready=self.ready,
-                               verify=self.verify)
+            try:
+                server = self.rcvr(port=self.port, handler=self.hdlr,
+                                   ready=self.ready,
+                                   verify=self.verify)
+            except BaseException:
+                # Do not leave the caller waiting for ready forever.
+                self.ready.set()
+                raise
             if self.port == 0:
                 self.port = server.server_address[1]
             self.ready.set()
index c1dc06e02d965781df0498a444aa0a333bd28a85..0c27c03ac85061058cb8ec5cf6fd4910397997c7 100644 (file)
@@ -3634,14 +3634,14 @@ class ConfigDictTest(BaseTest):
         # Ask for a randomly assigned port (by using port 0)
         t = logging.config.listen(0, verify)
         t.start()
-        t.ready.wait()
+        self.assertTrue(t.ready.wait(support.LONG_TIMEOUT),
+                        msg='the listener did not start')
         # Now get the port allocated
         port = t.port
         t.ready.clear()
         try:
-            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            sock.settimeout(2.0)
-            sock.connect(('localhost', port))
+            # The server can listen on IPv6, so do not force a family.
+            sock = socket.create_connection(('localhost', port), timeout=2.0)
 
             slen = struct.pack('>L', len(text))
             s = slen + text
@@ -3756,6 +3756,18 @@ class ConfigDictTest(BaseTest):
             ('ERROR', '2'),
         ], pat=r"^[\w.]+ -> (\w+): (\d+)$")
 
+    @support.requires_working_socket()
+    def test_listen_server_error(self):
+        # The "ready" event should be set even if the server fails to start.
+        t = logging.config.listen(-1)
+        t.daemon = True
+        with threading_helper.catch_threading_exception() as cm:
+            t.start()
+            self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT),
+                            msg='the listener did not report the failure')
+            threading_helper.join_thread(t)
+            self.assertIs(cm.exc_type, OverflowError)
+
     def test_bad_format(self):
         self.assertRaises(ValueError, self.apply_config, self.bad_format)
 
diff --git a/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst
new file mode 100644 (file)
index 0000000..27b6eea
--- /dev/null
@@ -0,0 +1,5 @@
+Fix :func:`logging.config.listen`: it left the caller waiting for the
+``ready`` event forever if the server could not be started,
+for example if the port was invalid or already in use.
+It now also binds to an IPv6 address if the host has no IPv4 address,
+for example if ``localhost`` is only aliased to ``::1``.