Skip to content

Commit 2cb04b9

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.14] gh-73458: Fix logging.config.listen() on a host without an IPv4 address (GH-154491) (GH-154617)
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 45a10f5) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0e8b607 commit 2cb04b9

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
@@ -1022,6 +1023,15 @@ class ConfigSocketReceiver(ThreadingTCPServer):
10221023

10231024
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
10241025
handler=None, ready=None, verify=None):
1026+
# The host can have no IPv4 address, for example if "localhost"
1027+
# is only aliased to ::1. Leave resolution errors to the server.
1028+
try:
1029+
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
1030+
except OSError:
1031+
pass
1032+
else:
1033+
if not any(info[0] == socket.AF_INET for info in infos):
1034+
self.address_family = infos[0][0]
10251035
ThreadingTCPServer.__init__(self, (host, port), handler)
10261036
with logging._lock:
10271037
self.abort = 0
@@ -1053,9 +1063,14 @@ def __init__(self, rcvr, hdlr, port, verify):
10531063
self.ready = threading.Event()
10541064

10551065
def run(self):
1056-
server = self.rcvr(port=self.port, handler=self.hdlr,
1057-
ready=self.ready,
1058-
verify=self.verify)
1066+
try:
1067+
server = self.rcvr(port=self.port, handler=self.hdlr,
1068+
ready=self.ready,
1069+
verify=self.verify)
1070+
except BaseException:
1071+
# Do not leave the caller waiting for ready forever.
1072+
self.ready.set()
1073+
raise
10591074
if self.port == 0:
10601075
self.port = server.server_address[1]
10611076
self.ready.set()

Lib/test/test_logging.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3634,14 +3634,14 @@ def setup_via_listener(self, text, verify=None):
36343634
# Ask for a randomly assigned port (by using port 0)
36353635
t = logging.config.listen(0, verify)
36363636
t.start()
3637-
t.ready.wait()
3637+
self.assertTrue(t.ready.wait(support.LONG_TIMEOUT),
3638+
msg='the listener did not start')
36383639
# Now get the port allocated
36393640
port = t.port
36403641
t.ready.clear()
36413642
try:
3642-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3643-
sock.settimeout(2.0)
3644-
sock.connect(('localhost', port))
3643+
# The server can listen on IPv6, so do not force a family.
3644+
sock = socket.create_connection(('localhost', port), timeout=2.0)
36453645

36463646
slen = struct.pack('>L', len(text))
36473647
s = slen + text
@@ -3756,6 +3756,18 @@ def verify_reverse(stuff):
37563756
('ERROR', '2'),
37573757
], pat=r"^[\w.]+ -> (\w+): (\d+)$")
37583758

3759+
@support.requires_working_socket()
3760+
def test_listen_server_error(self):
3761+
# The "ready" event should be set even if the server fails to start.
3762+
t = logging.config.listen(-1)
3763+
t.daemon = True
3764+
with threading_helper.catch_threading_exception() as cm:
3765+
t.start()
3766+
self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT),
3767+
msg='the listener did not report the failure')
3768+
threading_helper.join_thread(t)
3769+
self.assertIs(cm.exc_type, OverflowError)
3770+
37593771
def test_bad_format(self):
37603772
self.assertRaises(ValueError, self.apply_config, self.bad_format)
37613773

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)