]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-73458: Fix logging.config.listen() on a host without an IPv4 address (GH-154491)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 24 Jul 2026 15:00:40 +0000 (18:00 +0300)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 15:00:40 +0000 (18:00 +0300)
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.

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 35233e731eb42d1840311d972cc55943acf9c81a..fab91e663a0f6aa2cbd57c2c0d1427671c0fc3e9 100644 (file)
@@ -32,6 +32,7 @@ import logging.handlers
 import os
 import queue
 import re
+import socket
 import struct
 import threading
 import traceback
@@ -1004,6 +1005,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
@@ -1035,9 +1045,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 06b3aa66fc47a3151eedeef0b83cf589a9fb3316..3a26454d0b7ed8d3bd15c1a1a356ba710e9eeae2 100644 (file)
@@ -3643,14 +3643,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
@@ -3765,6 +3765,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``.