From 92fc5a56aa9af92ff3a7f17df4dc99e823e7182d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 22 Jul 2026 16:58:26 +0300 Subject: [PATCH 1/2] gh-73458: Fix logging.config.listen() on a host without an IPv4 address 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) --- Lib/logging/config.py | 21 ++++++++++++++++--- Lib/test/test_logging.py | 19 +++++++++++++---- ...6-07-22-13-01-24.gh-issue-73458.byxSPi.rst | 5 +++++ 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 35233e731eb42d1..fab91e663a0f6aa 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -32,6 +32,7 @@ import os import queue import re +import socket import struct import threading import traceback @@ -1004,6 +1005,15 @@ class ConfigSocketReceiver(ThreadingTCPServer): 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 __init__(self, rcvr, hdlr, port, verify): 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() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 06b3aa66fc47a31..afcbdf96150f8a7 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -3643,14 +3643,14 @@ def setup_via_listener(self, text, verify=None): # 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,17 @@ def verify_reverse(stuff): ('ERROR', '2'), ], pat=r"^[\w.]+ -> (\w+): (\d+)$") + 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 index 000000000000000..27b6eeac5b520b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst @@ -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``. From e64f981f8be6f908ff45319bf8e4bf0486f28b52 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 22 Jul 2026 18:56:24 +0300 Subject: [PATCH 2/2] Skip test_listen_server_error if sockets are not supported Like the other logging.config.listen() tests. It failed on WASI and Emscripten, which cannot start a thread. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_logging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index afcbdf96150f8a7..3a26454d0b7ed8d 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -3765,6 +3765,7 @@ def verify_reverse(stuff): ('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)