Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Lib/asyncio/unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,8 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None):
self._conn_lost = 0
self._closing = False # Set when close() or write_eof() called.

mode = os.fstat(self._fileno).st_mode
pipe_stat = os.fstat(self._fileno)
mode = pipe_stat.st_mode
is_char = stat.S_ISCHR(mode)
is_fifo = stat.S_ISFIFO(mode)
is_socket = stat.S_ISSOCK(mode)
Expand All @@ -657,7 +658,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None):
# On AIX, the reader trick (to be notified when the read end of the
# socket is closed) only works for sockets. On other platforms it
# works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.)
if is_socket or (is_fifo and not sys.platform.startswith("aix")):
# On macOS, the trick misfires for named FIFOs (but not for pipes
# created with os.pipe(), which have st_nlink == 0): the write end
# polls as readable whenever unread data sits in the FIFO, and no
# event is delivered when the read end is closed, so it can only
# ever report a false disconnection (gh-145030). The same xnu
# behaviour applies on iOS/tvOS/watchOS (sys.platform is not
# "darwin" there).
is_named_fifo_on_macos = (
sys.platform in {"darwin", "ios", "tvos", "watchos"}
and is_fifo and pipe_stat.st_nlink > 0)
if is_socket or (is_fifo
and not sys.platform.startswith("aix")
and not is_named_fifo_on_macos):
# only start reading when connection_made() has been called
self._loop.call_soon(self._loop._add_reader,
self._fileno, self._read_ready)
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,47 @@ def reader(data):
self.loop.run_until_complete(proto.done)
self.assertEqual('CLOSED', proto.state)

@unittest.skipUnless(sys.platform != 'win32',
"Don't support pipes for Windows")
@unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()')
def test_write_named_fifo_unread_data(self):
# gh-145030: on macOS, the write end of a named FIFO polls as
# readable while unread data sits in the FIFO, which made the
# transport misinterpret the event as the reader hanging up
# and close itself.
path = os_helper.TESTFN
os.mkfifo(path)
self.addCleanup(os_helper.unlink, path)
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
self.addCleanup(os.close, rfd)
wfd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
pipeobj = io.open(wfd, 'wb', 1024)

proto = MyWritePipeProto(loop=self.loop)
connect = self.loop.connect_write_pipe(lambda: proto, pipeobj)
transport, p = self.loop.run_until_complete(connect)
self.assertIs(p, proto)
self.assertEqual('CONNECTED', proto.state)

transport.write(b'1')
# Iterate the event loop while the data stays unread in the FIFO;
# the transport must not detect a false disconnection.
for _ in range(10):
test_utils.run_briefly(self.loop)
self.assertEqual('CONNECTED', proto.state)
self.assertFalse(transport.is_closing())
self.assertEqual(b'1', os.read(rfd, 1024))

transport.write(b'2345')
for _ in range(10):
test_utils.run_briefly(self.loop)
self.assertEqual('CONNECTED', proto.state)
self.assertEqual(b'2345', os.read(rfd, 1024))

transport.close()
self.loop.run_until_complete(proto.done)
self.assertEqual('CLOSED', proto.state)

@unittest.skipUnless(sys.platform != 'win32',
"Don't support pipes for Windows")
def test_write_pipe_disconnect_on_close(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread
data sitting in the FIFO made the transport misinterpret a poll event as
the reader disconnecting, wrongly closing the transport.
Loading