aioconsole

Asynchronous console and interfaces for asyncio

aioconsole provides:

  • asynchronous equivalents to input, exec and code.interact

  • an interactive loop running the asynchronous python console

  • a way to customize and run command line interface using argparse

  • stream support to serve interfaces instead of using standard streams

  • the apython script to access asyncio code at runtime without modifying the sources

Requirements

  • Python >= 3.8

Installation

aioconsole is available on PyPI and GitHub. Both of the following commands install the aioconsole package and the apython script.

$ pip3 install aioconsole   # from PyPI
$ python3 setup.py install  # or from the sources
$ apython -h
usage: apython [-h] [--serve [HOST:] PORT] [--no-readline]
               [--banner BANNER] [--locals LOCALS]
               [-m MODULE | FILE] ...

Asynchronous console

The example directory includes a slightly modified version of the echo server from the asyncio documentation. It runs an echo server on a given port and save the received messages in loop.history.

It runs fine and doesn’t use any aioconsole function:

$ python3 -m example.echo 8888
The echo service is being served on 127.0.0.1:8888

In order to access the program while it’s running, simply replace python3 with apython and redirect stdout so the console is not polluted by print statements (apython uses stderr):

$ apython -m example.echo 8888 > echo.log
Python 3.5.0 (default, Sep 7 2015, 14:12:03)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
---
This console is running in an asyncio event loop.
It allows you to wait for coroutines using the 'await' syntax.
Try: await asyncio.sleep(1, result=3, loop=loop)
---
>>>

This looks like the standard python console, with an extra message. It suggests using the await syntax (yield from for python 3.4):

>>> await asyncio.sleep(1, result=3, loop=loop)
# Wait one second...
3
>>>

The locals contain a reference to the event loop:

>>> dir()
['__doc__', '__name__', 'asyncio', 'loop']
>>> loop
<InteractiveEventLoop running=True closed=False debug=False>
>>>

So we can access the history of received messages:

>>> loop.history
defaultdict(<class 'list'>, {})
>>> sum(loop.history.values(), [])
[]

Let’s send a message to the server using a netcat client:

$ nc localhost 8888
Hello!
Hello!

The echo server behaves correctly. It is now possible to retrieve the message:

>>> sum(loop.history.values(), [])
['Hello!']

The console also supports Ctrl-C and Ctrl-D signals:

>>> ^C
KeyboardInterrupt
>>> # Ctrl-D
$

All this is implemented by setting InteractiveEventLoop as default event loop. It simply is a selector loop that schedules aioconsole.interact() coroutine when it’s created.

Serving the console

Moreover, aioconsole.interact() supports stream objects so it can be used along with asyncio.start_server to serve the python console. The aioconsole.start_interactive_server coroutine does exactly that. A backdoor can be introduced by simply adding the following line in the program:

server = await aioconsole.start_interactive_server(
    host='localhost', port=8000)

This is actually very similar to the eventlet.backdoor module. It is also possible to use the --serve option so it is not necessary to modify the code:

$ apython --serve :8889 -m example.echo 8888
The console is being served on 0.0.0.0:8889
The echo service is being served on 127.0.0.1:8888

Then connect using netcat and optionally, rlwrap:

$ rlwrap nc localhost 8889
Python 3.5.0 (default, Sep 7 2015, 14:12:03)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
---
This console is running in an asyncio event loop.
It allows you to wait for coroutines using the 'await' syntax.
Try: await asyncio.sleep(1, result=3, loop=loop)
---
>>>

Great! Anyone can now forkbomb your machine:

>>> import os
>>> os.system(':(){ :|:& };:')

Command line interfaces

The package also provides an AsychronousCli object. It is initialized with a dictionary of commands and can be scheduled with the coroutine async_cli.interact(). A dedicated command line interface to the echo server is defined in example/cli.py. In this case, the command dictionary is defined as:

commands = {'history': (get_history, parser)}

where get_history is a coroutine and parser an ArgumentParser from the argparse module. The arguments of the parser will be passed as keywords arguments to the coroutine.

Let’s run the command line interface:

$ python3 -m example.cli 8888 > cli.log
Welcome to the CLI interface of echo!
Try:
* 'help' to display the help message
* 'list' to display the command list.
>>>

The help and list commands are generated automatically:

>>> help
Type 'help' to display this message.
Type 'list' to display the command list.
Type '<command> -h' to display the help message of <command>.
>>> list
List of commands:
 * help [-h]
 * history [-h] [--pattern PATTERN]
 * list [-h]
>>>

The history command defined earlier can be found in the list. Note that it has an help option and a pattern argument:

>>> history -h
usage: history [-h] [--pattern PATTERN]

Display the message history

optional arguments:
  -h, --help            show this help message and exit
  --pattern PATTERN, -p PATTERN
                        pattern to filter hostnames

Example usage of the history command:

>>> history
No message in the history
>>> # A few messages later
>>> history
Host 127.0.0.1:
  0. Hello!
  1. Bye!
Host 192.168.0.3
  0. Sup!
>>> history -p 127.*
Host 127.0.0.1:
  0. Hello!
  1. Bye!

Serving interfaces

Just like asyncio.interact(), AsynchronousCli can be initialized with any pair of streams. It can be used along with asyncio.start_server to serve the command line interface. The previous example provides this functionality through the --serve-cli option:

$ python3 -m example.cli 8888 --serve-cli 8889
The command line interface is being served on 127.0.0.1:8889
The echo service is being served on 127.0.0.1:8888

It’s now possible to access the interface using netcat:

$ rlwrap nc localhost 8889
Welcome to the CLI interface of echo!
Try:
 * 'help' to display the help message
 * 'list' to display the command list.
>>>

It is also possible to combine the example with the apython script to add an extra access for debugging:

$ apython --serve 8887 -m example.cli 8888 --serve-cli 8889
The console is being served on 127.0.0.1:8887
The command line interface is being served on 127.0.0.1:8889
The echo service is being served on 127.0.0.1:8888

Limitations

The python console exposed by aioconsole is quite limited compared to modern consoles such as IPython or ptpython. Luckily, those projects gained greater asyncio support over the years. In particular, the following use cases overlap with aioconsole capabilities:

Contact

Vincent Michel: vxgmichel@gmail.com