aioconsole icon indicating copy to clipboard operation
aioconsole copied to clipboard

Can't mix ainput() with input() on Unix

Open plammens opened this issue 3 years ago • 6 comments

On Linux, trying to use regular blocking input() after a call to ainput() fails with EOFError.

Minimal example:

import asyncio
from aioconsole import ainput


async def main():
    await ainput("ainput: ")
    input("input: ")


asyncio.run(main())

On Ubuntu 20.04 (tested in WSL), if you run this and enter something at the first prompt (ainput: ), then the input() call fails immediately with EOFError.

On the other hand, this works fine on Windows.

If the input() call is made before ainput(), then there are no problems.


This seems to be due to the fact that ainput() sets the O_NONBLOCK flag in stdin/stdout. Indeed, if this flag is cleared after the call to ainput(), then input() works fine again:

import asyncio
import fcntl
import sys

from aioconsole import ainput


async def main():
    await ainput("ainput: ")
    fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, 0)
    input("input: ")


asyncio.run(main())

Maybe this should be done automatically by ainput.

plammens avatar Jun 08 '22 07:06 plammens

Hi @plammens and thanks for your report!

We already had a similar discussion here: https://github.com/vxgmichel/aioconsole/issues/90#issuecomment-997997618

Still, your suggestion about setting the non-blocking mode only when necessary is interesting, I'll think about it :)

However and as I said in the comment mentioned above, I would advise to simply avoid using input() from an asyncio coroutine as it would block the event loop while waiting for the user input.

vxgmichel avatar Jun 09 '22 17:06 vxgmichel

Thanks @vxgmichel, sorry hadn't seen #90, it's the same issue!

I also wanted to add that I've found that when using ainput, you shouldn't use regular input but neither regular print, the latter because it risks raising a BlockingIOError if the buffer is full (e.g. if you try to print a lot of stuff) since stdout is in non-blocking mode.

await ainput()
print('a' * 10**7)  # BlockingIOError

So currently, if you use ainput once you should always use ainput/aprint and never input/print.

plammens avatar Jun 10 '22 06:06 plammens

Regarding my suggestion, I realised it might lead to problems if multiple ainputs are being awaited simultaneously (is that even possible?):

  1. The first ainput sets non-blocking mode and starts waiting for input
  2. The second ainput sets non-blocking mode (no-op) and starts waiting for input
  3. User enters input
  4. The first ainput returns, sets blocking mode

Would that mess up the second one, which is still waiting?

plammens avatar Jun 10 '22 06:06 plammens

Would that mess up the second one, which is still waiting?

Yes it would, so the implementation would have to take care of that. Another solution would be to patch builtins.print and builtins.input to add a warning but patching stdlib is often more annoying than helpful. I'm not sure what to do about this issue :thinking:

vxgmichel avatar Jun 10 '22 14:06 vxgmichel

This is still causing havoc for us in the form of logging errors:

--- Logging error ---
Traceback (most recent call last):
  File "/usr/lib/python3.11/logging/__init__.py", line 1113, in emit
    stream.write(msg + self.terminator)
BlockingIOError: [Errno 11] write could not complete without blocking
Call stack:
  File "<string>", line 1, in <module>
  File "/home/bls/Downloads/code/bbot/bbot/cli.py", line 387, in main
    asyncio.run(_main())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "/usr/lib/python3.11/asyncio/base_events.py", line 640, in run_until_complete
    self.run_forever()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/home/bls/Downloads/code/bbot/bbot/modules/output/human.py", line 40, in handle_event
    self.stdout(event_str)
  File "/home/bls/Downloads/code/bbot/bbot/modules/base.py", line 1175, in stdout
    self.log.stdout(*args, extra={"scan_id": self.scan.id}, **kwargs)
  File "/home/bls/Downloads/code/bbot/bbot/core/logger/logger.py", line 83, in logForLevel
    self._log(levelNum, message, args, **kwargs)

I understand the need for stdin to be set to non-blocking mode, but is there a way to ensure that doesn't happen to stdout?

TheTechromancer avatar Feb 27 '24 19:02 TheTechromancer

Discovered this isn't an issue with aioconsole. The behavior is coming from somwhere inside asyncio. The following code is enough to set stdout to non-blocking mode (note in the code there is no reference to stdout):

reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)

TheTechromancer avatar Feb 27 '24 21:02 TheTechromancer