Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
plammens opened this issue Jun 8, 2022 · 6 comments
Open

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

plammens opened this issue Jun 8, 2022 · 6 comments

Comments

@plammens
Copy link

plammens commented Jun 8, 2022

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.

@vxgmichel
Copy link
Owner

Hi @plammens and thanks for your report!

We already had a similar discussion here: #90 (comment)

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.

@plammens
Copy link
Author

plammens commented Jun 10, 2022

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
Copy link
Author

plammens commented Jun 10, 2022

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?

@vxgmichel
Copy link
Owner

vxgmichel commented Jun 10, 2022

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 🤔

@TheTechromancer
Copy link

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
Copy link

TheTechromancer commented Feb 27, 2024

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants