Skip to content

Commit 2bdc27e

Browse files
[3.15] gh-145030: Fix asyncio write pipe transport for named FIFOs on macOS (GH-154222) (#154613)
gh-145030: Fix asyncio write pipe transport for named FIFOs on macOS (GH-154222) (cherry picked from commit 409f324) Co-authored-by: Bhuvi <b.chouksey27@gmail.com>
1 parent c867740 commit 2bdc27e

3 files changed

Lines changed: 60 additions & 2 deletions

File tree

Lib/asyncio/unix_events.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,8 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None):
639639
self._conn_lost = 0
640640
self._closing = False # Set when close() or write_eof() called.
641641

642-
mode = os.fstat(self._fileno).st_mode
642+
pipe_stat = os.fstat(self._fileno)
643+
mode = pipe_stat.st_mode
643644
is_char = stat.S_ISCHR(mode)
644645
is_fifo = stat.S_ISFIFO(mode)
645646
is_socket = stat.S_ISSOCK(mode)
@@ -656,7 +657,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None):
656657
# On AIX, the reader trick (to be notified when the read end of the
657658
# socket is closed) only works for sockets. On other platforms it
658659
# works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.)
659-
if is_socket or (is_fifo and not sys.platform.startswith("aix")):
660+
# On macOS, the trick misfires for named FIFOs (but not for pipes
661+
# created with os.pipe(), which have st_nlink == 0): the write end
662+
# polls as readable whenever unread data sits in the FIFO, and no
663+
# event is delivered when the read end is closed, so it can only
664+
# ever report a false disconnection (gh-145030). The same xnu
665+
# behaviour applies on iOS/tvOS/watchOS (sys.platform is not
666+
# "darwin" there).
667+
is_named_fifo_on_apple = (
668+
sys.platform in {"darwin", "ios", "tvos", "watchos"}
669+
and is_fifo and pipe_stat.st_nlink > 0)
670+
if is_socket or (is_fifo
671+
and not sys.platform.startswith("aix")
672+
and not is_named_fifo_on_apple):
660673
# only start reading when connection_made() has been called
661674
self._loop.call_soon(self._loop._add_reader,
662675
self._fileno, self._read_ready)

Lib/test/test_asyncio/test_events.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,48 @@ def reader(data):
17251725
self.loop.run_until_complete(proto.done)
17261726
self.assertEqual('CLOSED', proto.state)
17271727

1728+
@unittest.skipUnless(sys.platform != 'win32',
1729+
"Don't support pipes for Windows")
1730+
@unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()')
1731+
def test_write_named_fifo_unread_data(self):
1732+
# gh-145030: on macOS, the write end of a named FIFO polls as
1733+
# readable while unread data sits in the FIFO, which made the
1734+
# transport misinterpret the event as the reader hanging up
1735+
# and close itself.
1736+
path = os_helper.TESTFN
1737+
os.mkfifo(path)
1738+
self.assertNotEqual(os.stat(path).st_nlink, 0)
1739+
self.addCleanup(os_helper.unlink, path)
1740+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
1741+
self.addCleanup(os.close, rfd)
1742+
wfd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
1743+
pipeobj = io.open(wfd, 'wb', 1024)
1744+
1745+
proto = MyWritePipeProto(loop=self.loop)
1746+
connect = self.loop.connect_write_pipe(lambda: proto, pipeobj)
1747+
transport, p = self.loop.run_until_complete(connect)
1748+
self.assertIs(p, proto)
1749+
self.assertEqual('CONNECTED', proto.state)
1750+
1751+
transport.write(b'1')
1752+
# Iterate the event loop while the data stays unread in the FIFO;
1753+
# the transport must not detect a false disconnection.
1754+
for _ in range(10):
1755+
test_utils.run_briefly(self.loop)
1756+
self.assertEqual('CONNECTED', proto.state)
1757+
self.assertFalse(transport.is_closing())
1758+
self.assertEqual(b'1', os.read(rfd, 1024))
1759+
1760+
transport.write(b'2345')
1761+
for _ in range(10):
1762+
test_utils.run_briefly(self.loop)
1763+
self.assertEqual('CONNECTED', proto.state)
1764+
self.assertEqual(b'2345', os.read(rfd, 1024))
1765+
1766+
transport.close()
1767+
self.loop.run_until_complete(proto.done)
1768+
self.assertEqual('CLOSED', proto.state)
1769+
17281770
@unittest.skipUnless(sys.platform != 'win32',
17291771
"Don't support pipes for Windows")
17301772
def test_write_pipe_disconnect_on_close(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread
2+
data sitting in the FIFO made the transport misinterpret a poll event as
3+
the reader disconnecting, wrongly closing the transport.

0 commit comments

Comments
 (0)