Skip to content

Commit 624c704

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.13] gh-73458: Fix logging.config.listen() on a host without an IPv4 address (GH-154491) (GH-154618)
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 fa16a75 commit 624c704

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
@@ -1010,6 +1011,15 @@ class ConfigSocketReceiver(ThreadingTCPServer):
10101011

10111012
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
10121013
handler=None, ready=None, verify=None):
1014+
# The host can have no IPv4 address, for example if "localhost"
1015+
# is only aliased to ::1. Leave resolution errors to the server.
1016+
try:
1017+
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
1018+
except OSError:
1019+
pass
1020+
else:
1021+
if not any(info[0] == socket.AF_INET for info in infos):
1022+
self.address_family = infos[0][0]
10131023
ThreadingTCPServer.__init__(self, (host, port), handler)
10141024
with logging._lock:
10151025
self.abort = 0
@@ -1041,9 +1051,14 @@ def __init__(self, rcvr, hdlr, port, verify):
10411051
self.ready = threading.Event()
10421052

10431053
def run(self):
1044-
server = self.rcvr(port=self.port, handler=self.hdlr,
1045-
ready=self.ready,
1046-
verify=self.verify)
1054+
try:
1055+
server = self.rcvr(port=self.port, handler=self.hdlr,
1056+
ready=self.ready,
1057+
verify=self.verify)
1058+
except BaseException:
1059+
# Do not leave the caller waiting for ready forever.
1060+
self.ready.set()
1061+
raise
10471062
if self.port == 0:
10481063
self.port = server.server_address[1]
10491064
self.ready.set()

Lib/test/test_logging.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,14 +3584,14 @@ def setup_via_listener(self, text, verify=None):
35843584
# Ask for a randomly assigned port (by using port 0)
35853585
t = logging.config.listen(0, verify)
35863586
t.start()
3587-
t.ready.wait()
3587+
self.assertTrue(t.ready.wait(support.LONG_TIMEOUT),
3588+
msg='the listener did not start')
35883589
# Now get the port allocated
35893590
port = t.port
35903591
t.ready.clear()
35913592
try:
3592-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3593-
sock.settimeout(2.0)
3594-
sock.connect(('localhost', port))
3593+
# The server can listen on IPv6, so do not force a family.
3594+
sock = socket.create_connection(('localhost', port), timeout=2.0)
35953595

35963596
slen = struct.pack('>L', len(text))
35973597
s = slen + text
@@ -3706,6 +3706,18 @@ def verify_reverse(stuff):
37063706
('ERROR', '2'),
37073707
], pat=r"^[\w.]+ -> (\w+): (\d+)$")
37083708

3709+
@support.requires_working_socket()
3710+
def test_listen_server_error(self):
3711+
# The "ready" event should be set even if the server fails to start.
3712+
t = logging.config.listen(-1)
3713+
t.daemon = True
3714+
with threading_helper.catch_threading_exception() as cm:
3715+
t.start()
3716+
self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT),
3717+
msg='the listener did not report the failure')
3718+
threading_helper.join_thread(t)
3719+
self.assertIs(cm.exc_type, OverflowError)
3720+
37093721
def test_bad_format(self):
37103722
self.assertRaises(ValueError, self.apply_config, self.bad_format)
37113723

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)