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