Skip to content

Commit 45a10f5

Browse files
gh-73458: Fix logging.config.listen() on a host without an IPv4 address (GH-154491)
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>
1 parent 74f4b06 commit 45a10f5

3 files changed

Lines changed: 39 additions & 7 deletions

File tree

Lib/logging/config.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import os
3333
import queue
3434
import re
35+
import socket
3536
import struct
3637
import threading
3738
import traceback
@@ -1004,6 +1005,15 @@ class ConfigSocketReceiver(ThreadingTCPServer):
10041005

10051006
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
10061007
handler=None, ready=None, verify=None):
1008+
# The host can have no IPv4 address, for example if "localhost"
1009+
# is only aliased to ::1. Leave resolution errors to the server.
1010+
try:
1011+
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
1012+
except OSError:
1013+
pass
1014+
else:
1015+
if not any(info[0] == socket.AF_INET for info in infos):
1016+
self.address_family = infos[0][0]
10071017
ThreadingTCPServer.__init__(self, (host, port), handler)
10081018
with logging._lock:
10091019
self.abort = 0
@@ -1035,9 +1045,14 @@ def __init__(self, rcvr, hdlr, port, verify):
10351045
self.ready = threading.Event()
10361046

10371047
def run(self):
1038-
server = self.rcvr(port=self.port, handler=self.hdlr,
1039-
ready=self.ready,
1040-
verify=self.verify)
1048+
try:
1049+
server = self.rcvr(port=self.port, handler=self.hdlr,
1050+
ready=self.ready,
1051+
verify=self.verify)
1052+
except BaseException:
1053+
# Do not leave the caller waiting for ready forever.
1054+
self.ready.set()
1055+
raise
10411056
if self.port == 0:
10421057
self.port = server.server_address[1]
10431058
self.ready.set()

Lib/test/test_logging.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3643,14 +3643,14 @@ def setup_via_listener(self, text, verify=None):
36433643
# Ask for a randomly assigned port (by using port 0)
36443644
t = logging.config.listen(0, verify)
36453645
t.start()
3646-
t.ready.wait()
3646+
self.assertTrue(t.ready.wait(support.LONG_TIMEOUT),
3647+
msg='the listener did not start')
36473648
# Now get the port allocated
36483649
port = t.port
36493650
t.ready.clear()
36503651
try:
3651-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3652-
sock.settimeout(2.0)
3653-
sock.connect(('localhost', port))
3652+
# The server can listen on IPv6, so do not force a family.
3653+
sock = socket.create_connection(('localhost', port), timeout=2.0)
36543654

36553655
slen = struct.pack('>L', len(text))
36563656
s = slen + text
@@ -3765,6 +3765,18 @@ def verify_reverse(stuff):
37653765
('ERROR', '2'),
37663766
], pat=r"^[\w.]+ -> (\w+): (\d+)$")
37673767

3768+
@support.requires_working_socket()
3769+
def test_listen_server_error(self):
3770+
# The "ready" event should be set even if the server fails to start.
3771+
t = logging.config.listen(-1)
3772+
t.daemon = True
3773+
with threading_helper.catch_threading_exception() as cm:
3774+
t.start()
3775+
self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT),
3776+
msg='the listener did not report the failure')
3777+
threading_helper.join_thread(t)
3778+
self.assertIs(cm.exc_type, OverflowError)
3779+
37683780
def test_bad_format(self):
37693781
self.assertRaises(ValueError, self.apply_config, self.bad_format)
37703782

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :func:`logging.config.listen`: it left the caller waiting for the
2+
``ready`` event forever if the server could not be started,
3+
for example if the port was invalid or already in use.
4+
It now also binds to an IPv6 address if the host has no IPv4 address,
5+
for example if ``localhost`` is only aliased to ``::1``.

0 commit comments

Comments
 (0)