diff --git a/gns3server/main.py b/gns3server/main.py index 07b18201..10ed2bf9 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -30,6 +30,7 @@ import gns3server.utils.get_resource import os import sys +import types # To avoid strange bug later we switch the event loop before any other operation if sys.platform.startswith("win"): @@ -38,6 +39,9 @@ if sys.platform.startswith("win"): loop = asyncio.ProactorEventLoop() asyncio.set_event_loop(loop) +if sys.platform.startswith("win"): + sys.modules['termios'] = types.ModuleType('termios') + def daemonize(): """ diff --git a/gns3server/utils/asyncio/embed_shell.py b/gns3server/utils/asyncio/embed_shell.py index fac81d3f..6c7d9828 100644 --- a/gns3server/utils/asyncio/embed_shell.py +++ b/gns3server/utils/asyncio/embed_shell.py @@ -20,7 +20,18 @@ import sys import asyncio import inspect -from .telnet_server import AsyncioTelnetServer +from prompt_toolkit import prompt +from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.contrib.completers import WordCompleter +from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.eventloop.base import EventLoop +from prompt_toolkit.interface import CommandLineInterface +from prompt_toolkit.layout.screen import Size +from prompt_toolkit.shortcuts import create_prompt_application, create_asyncio_eventloop +from prompt_toolkit.terminal.vt100_output import Vt100_Output + +from .telnet_server import AsyncioTelnetServer, TelnetConnection +from .input_stream import InputStream class EmbedShell: @@ -60,6 +71,14 @@ class EmbedShell: def prompt(self, val): self._prompt = val + @property + def welcome_message(self): + return self._welcome_message + + @welcome_message.setter + def welcome_message(self, welcome_message): + self._welcome_message = welcome_message + @asyncio.coroutine def help(self, *args): """ @@ -90,6 +109,11 @@ class EmbedShell: found = False if cmd[0] == '?': cmd[0] = 'help' + + # when there is no command specified just return empty result + if not cmd[0].strip(): + return "" + for (name, meth) in inspect.getmembers(self): if name == cmd[0]: cmd.pop(0) @@ -97,7 +121,7 @@ class EmbedShell: found = True break if not found: - res = ('Command not found {}'.format(cmd[0]) + (yield from self.help())) + res = ('Command not found {}\n'.format(cmd[0]) + (yield from self.help())) return res @asyncio.coroutine @@ -111,29 +135,140 @@ class EmbedShell: res = yield from self._parse_command(result) self._writer.feed_data(res.encode()) + def get_commands(self): + """ + Returns commands available to execute + :return: list of (name, doc) tuples + """ + commands = [] + for name, value in inspect.getmembers(self): + if not inspect.isgeneratorfunction(value): + continue + if name.startswith('_') or name == 'run': + continue + doc = inspect.getdoc(value) + commands.append((name, doc)) + return commands + + +class UnstoppableEventLoop(EventLoop): + """ + Partially fake event loop which cannot be stopped by CommandLineInterface + """ + def __init__(self, loop): + self._loop = loop + + def close(self): + " Ignore. " + + def stop(self): + " Ignore. " + + def run_in_executor(self, *args, **kwargs): + return self._loop.run_in_executor(*args, **kwargs) + + def call_from_executor(self, callback, **kwargs): + self._loop.call_from_executor(callback, **kwargs) + + def add_reader(self, fd, callback): + raise NotImplementedError + + def remove_reader(self, fd): + raise NotImplementedError + + +class ShellConnection(TelnetConnection): + def __init__(self, reader, writer, shell, loop): + super(ShellConnection, self).__init__(reader, writer) + self._shell = shell + self._loop = loop + self._cli = None + self._cb = None + self._size = Size(rows=40, columns=79) + self.encoding = 'utf-8' + + + @asyncio.coroutine + def connected(self): + def get_size(): + return self._size + + self._cli = CommandLineInterface( + application=create_prompt_application(self._shell.prompt), + eventloop=UnstoppableEventLoop(create_asyncio_eventloop(self._loop)), + output=Vt100_Output(self, get_size)) + + self._cb = self._cli.create_eventloop_callbacks() + self._inputstream = InputStream(self._cb.feed_key) + # Taken from prompt_toolkit telnet server + # https://github.com/jonathanslenders/python-prompt-toolkit/blob/99fa7fae61c9b4ed9767ead3b4f9b1318cfa875d/prompt_toolkit/contrib/telnet/server.py#L165 + self._cli._is_running = True + + if self._shell.welcome_message is not None: + self.send(self._shell.welcome_message.encode()) + + self._cli._redraw() + + @asyncio.coroutine + def disconnected(self): + pass + + def window_size_changed(self, columns, rows): + self._size = Size(rows=rows, columns=columns) + self._cb.terminal_size_changed() + + @asyncio.coroutine + def feed(self, data): + data = data.decode() + self._inputstream.feed(data) + self._cli._redraw() + + # Prompt toolkit has returned the command + if self._cli.is_returning: + try: + returned_value = self._cli.return_value() + except (EOFError, KeyboardInterrupt) as e: + # don't close terminal, just keep it alive + self.close() + return + + command = returned_value.text + + res = yield from self._shell._parse_command(command) + self.send(res.encode()) + self.reset() + + def reset(self): + """ Resets terminal screen""" + self._cli.reset() + self._cli.buffers[DEFAULT_BUFFER].reset() + self._cli.renderer.request_absolute_cursor_position() + self._cli._redraw() + + def write(self, data): + """ Compat with CLI""" + self.send(data) + + def flush(self): + """ Compat with CLI""" + pass + def create_telnet_shell(shell, loop=None): """ Run a shell application with a telnet frontend - :param application: An EmbedShell instance :param loop: The event loop :returns: Telnet server """ - class Stream(asyncio.StreamReader): - def write(self, data): - self.feed_data(data) - - @asyncio.coroutine - def drain(self): - pass - shell.reader = Stream() - shell.writer = Stream() if loop is None: loop = asyncio.get_event_loop() - loop.create_task(shell.run()) - return AsyncioTelnetServer(reader=shell.writer, writer=shell.reader, binary=False, echo=False) + + def factory(reader, writer): + return ShellConnection(reader, writer, shell, loop) + + return AsyncioTelnetServer(binary=True, echo=True, naws=True, connection_factory=factory) def create_stdin_shell(shell, loop=None): @@ -145,9 +280,13 @@ def create_stdin_shell(shell, loop=None): :returns: Telnet server """ @asyncio.coroutine - def feed_stdin(loop, reader): + def feed_stdin(loop, reader, shell): + history = InMemoryHistory() + completer = WordCompleter([name for name, _ in shell.get_commands()], ignore_case=True) while True: - line = yield from loop.run_in_executor(None, sys.stdin.readline) + line = yield from prompt( + ">", patch_stdout=True, return_asyncio_coroutine=True, history=history, completer=completer) + line += '\n' reader.feed_data(line.encode()) @asyncio.coroutine @@ -164,7 +303,7 @@ def create_stdin_shell(shell, loop=None): if loop is None: loop = asyncio.get_event_loop() - reader_task = loop.create_task(feed_stdin(loop, reader)) + reader_task = loop.create_task(feed_stdin(loop, reader, shell)) writer_task = loop.create_task(read_stdout(writer)) shell_task = loop.create_task(shell.run()) return asyncio.gather(shell_task, writer_task, reader_task) @@ -181,20 +320,26 @@ if __name__ == '__main__': This command accept arguments: hello tutu will display tutu """ - if len(args): - return ' '.join(args) - else: - return 'world\n' + @asyncio.coroutine + def world(): + yield from asyncio.sleep(2) + if len(args): + return ' '.join(args) + else: + return 'world\n' + + return (yield from world()) # Demo using telnet - # server = create_telnet_shell(Demo()) - # coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) - # s = loop.run_until_complete(coro) - # try: - # loop.run_forever() - # except KeyboardInterrupt: - # pass + shell = Demo(welcome_message="Welcome!\n") + server = create_telnet_shell(shell, loop=loop) + coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) + s = loop.run_until_complete(coro) + try: + loop.run_forever() + except KeyboardInterrupt: + pass # Demo using stdin - loop.run_until_complete(create_stdin_shell(Demo())) - loop.close() + # loop.run_until_complete(create_stdin_shell(Demo())) + # loop.close() diff --git a/gns3server/utils/asyncio/input_stream.py b/gns3server/utils/asyncio/input_stream.py new file mode 100644 index 00000000..c80dc8f6 --- /dev/null +++ b/gns3server/utils/asyncio/input_stream.py @@ -0,0 +1,419 @@ +""" +Parser for VT100 input stream. +""" + +# Copied from prompt_toolkit/terminal/vt100_input.py due to dependency on termios (which is not available on Windows) + +from __future__ import unicode_literals + +import re +import six + +from six.moves import range + +from prompt_toolkit.keys import Keys +from prompt_toolkit.key_binding.input_processor import KeyPress + +__all__ = ( + 'InputStream', + 'raw_mode', + 'cooked_mode', +) + +_DEBUG_RENDERER_INPUT = False +_DEBUG_RENDERER_INPUT_FILENAME = 'prompt-toolkit-render-input.log' + + +# Regex matching any CPR response +# (Note that we use '\Z' instead of '$', because '$' could include a trailing +# newline.) +_cpr_response_re = re.compile('^' + re.escape('\x1b[') + r'\d+;\d+R\Z') + +# Mouse events: +# Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M" +_mouse_event_re = re.compile('^' + re.escape('\x1b[') + r'(=5.23.0 psutil>=3.0.0 zipstream>=1.1.4 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 +prompt-toolkit