-
-
Notifications
You must be signed in to change notification settings - Fork 34.1k
Description
Bug report
Bug description:
When hooking up an asyncio.streams.streamWriter to a fifo on macOS, the 'reader trick' seems to backfire.
The read event seems to fire immediately (well, at least as soon as it has a chance to execute), meaning _UnixWritePipeTransport goes straight to closed state. it does seem to need the write/drain to happen to trigger this. Maybe both ends of the fifo are going readable after the write?
I'm not 100% certain what conditions cause this to happen, but I can reliably reproduce this just with cat reading the other end of the fifo.
This seems loosely related to #109757, as well as maybe #63492 #63493
It possible to work around this by hacking in an is_fifo=False at unix_events.py:657, i.e. just before this code, cancelling registering the _add_reader event -
if is_socket or (is_fifo and not sys.platform.startswith("aix")):
# only start reading when connection_made() has been called
self._loop.call_soon(self._loop._add_reader,
self._fileno, self._read_ready)
Tested with Python 3.14.3 (release), installed via Homebrew. This is on the latest production macOS (26.3 (25D125)) -
% uname -a
Darwin lappy 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:05 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6020 arm64
For the record, I also tested this code on Linux, and it works fine. This was with Python 3.13.12 on kernel -
Linux lappy-debian2 6.18.5+deb14-arm64 #1 SMP PREEMPT Debian 6.18.5-1 (2026-01-16) aarch64 GNU/Linux
Ultimately, like the other related issues this seems like a platform problem - though I don't have an equivalent of strace to check this at the syscall level. So I guess I'm wondering if there's a way to work around it - or if I'm doing something wrong. Is there a way to switch _UnixWritePipeTransport into "AIX mode" so it ignores the read event for fifos without modifying the code already there?
There's reproducer code and example output below -
# client.py
import asyncio
import os
async def write_msg(writer):
print("writing ... is_closing? ", writer.is_closing())
writer.write(bytearray("MSG\n", "utf-8"))
print("draining ... is_closing? ", writer.is_closing())
await writer.drain()
print("drain done ... is_closing? ", writer.is_closing())
async def main():
# terminology is from server's POV - ie stdout is server's stdout
stdin_fd = open("fifo_in", mode="w")
print("fifo_in open fd=%d" % stdin_fd.fileno())
os.set_blocking(stdin_fd.fileno(), False)
loop = asyncio.events.get_running_loop()
# explosion at unix_events.py:658
writer_transport, writer_protocol = await loop.connect_write_pipe(
lambda: asyncio.streams.FlowControlMixin(loop=loop), stdin_fd
)
writer = asyncio.streams.StreamWriter(
writer_transport, writer_protocol, None, loop=loop
)
while True:
await asyncio.create_task(write_msg(writer))
print("task done... is_closing? ", writer.is_closing())
asyncio.run(main())
Running and output -
% mkfifo fifo_in
% python3 client.py
fifo_in open fd=6
writing ... is_closing? False
draining ... is_closing? False
drain done ... is_closing? False
task done... is_closing? False
writing ... is_closing? True
draining ... is_closing? True
Traceback (most recent call last):
File "/Volumes/devel/src/py_fifos/client.py", line 36, in <module>
asyncio.run(main())
~~~~~~~~~~~^^^^^^^^
File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/runners.py", line 204, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/runners.py", line 127, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/base_events.py", line 719, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "/Volumes/devel/src/py_fifos/client.py", line 32, in main
await asyncio.create_task(write_msg(writer))
File "/Volumes/devel/src/py_fifos/client.py", line 9, in write_msg
await writer.drain()
File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/streams.py", line 386, in drain
await self._protocol._drain_helper()
File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/streams.py", line 166, in _drain_helper
raise ConnectionResetError('Connection lost')
ConnectionResetError: Connection lost
The other end of the pipe is just cat reading in a different terminal window...
% cat <fifo_in
MSG
It doesn't matter whether client.py or cat is invoked first - the behaviour is the same..
CPython versions tested on:
3.14, 3.13
Operating systems tested on:
macOS, Linux
Metadata
Metadata
Assignees
Labels
Projects
Status