From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.6 required=3.0 tests=BAYES_00, DKIM_ADSP_CUSTOM_MED,DKIM_INVALID,DKIM_SIGNED,FREEMAIL_FORGED_FROMDOMAIN, FREEMAIL_FROM,HEADER_FROM_DIFFERENT_DOMAINS,HTML_MESSAGE,INCLUDES_CR_TRAILER, INCLUDES_PATCH,MAILING_LIST_MULTI,MENTIONS_GIT_HOSTING,SPF_HELO_NONE,SPF_PASS autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id A2F0EC6377B for ; Wed, 21 Jul 2021 20:26:01 +0000 (UTC) Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 2840361221 for ; Wed, 21 Jul 2021 20:26:01 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mail.kernel.org 2840361221 Authentication-Results: mail.kernel.org; dmarc=fail (p=none dis=none) header.from=gmail.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Received: from localhost ([::1]:35296 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1m6In2-0000SP-5l for qemu-devel@archiver.kernel.org; Wed, 21 Jul 2021 16:26:00 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:41706) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1m6IkO-0004iV-04 for qemu-devel@nongnu.org; Wed, 21 Jul 2021 16:23:16 -0400 Received: from mail-il1-x130.google.com ([2607:f8b0:4864:20::130]:42524) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1m6IkJ-00040n-Sh for qemu-devel@nongnu.org; Wed, 21 Jul 2021 16:23:15 -0400 Received: by mail-il1-x130.google.com with SMTP id m20so3405187ili.9 for ; Wed, 21 Jul 2021 13:23:11 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=mime-version:references:in-reply-to:from:date:message-id:subject:to :cc; bh=YB6XKW0CqKcv9GJkQzp6fLg1PUa61wJnU3ZA3raU9q0=; b=DTtK5DW20L/wLpIO+cF5QxAcT+/vTAT/rB+nFmmK2RAKUI8iizCV645Irh9S52soip qSFasiIwvZ41DiYWUcjfUJHEPqxqQZ3b+bjnlFZfMLhrtIyn/chru2GgbgkcbjoyQJ99 2pJx3LPsCtCY+o3jS1JqXmV7qSPdxp1Z1knrtekKH6dS6lLGB0I15K+dD489FoLF9z6T FnjHgNP/EiBBlTv249B86SSsVvK2xIY2RK5dU4osGp/N0Mv175N6bjYG9IC5oOOD7KuM lwoZ9qtFc+LIxfmLBOcMs53xpiksWiBrCfDd5yphdw0/rAK6c6zVkaO37JcM9MTAdCgC EZCg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:references:in-reply-to:from:date :message-id:subject:to:cc; bh=YB6XKW0CqKcv9GJkQzp6fLg1PUa61wJnU3ZA3raU9q0=; b=da+EkZwIz9p4yjtbCwk3IsTW6R0cfMOVlU7GUd9ilhHq6U/TgYDnmLYIpZLBfNoaWn c6NO/16knsQ6EUwKexPWvMJpqq8/osTcQMQN22Qr+/UPh4zrszPjmLEZ0FoeU1OU6KRy jBT9e2Vza7lSI5c0rjPnZQKK7FATq8nf0iedjE8zM646APdpFK9P55x6U15JNhYS6+6k KNPPaARfGJnzWQ9/dWJ/3I91vc2h7q8/pdZOT4zoubY4NnSBdl+lReXRiKFnT8YoxA+b ALq3wMI2OBUqnrQh+NC0vCH/y7mGEFgLZeVQpI2yHR6OitqV0XG7UnSddn/ufakVjeEC sBfg== X-Gm-Message-State: AOAM532DbzcA1W36E8HCj4+dm7tK8U0AGCwmk0AWow9qXxb6UZU1+G3W //3ROCoJXkUofLXSnOHe3x1zMtZUYlXU/52zn44= X-Google-Smtp-Source: ABdhPJzDGq186iZd+TKgLO7ZMHtnpC4vcaw69VuRc89PT44TWd5qa5jF6aXhPhL7zqrZabiAAzdp9sxgWsGWnNwGM8w= X-Received: by 2002:a92:db4e:: with SMTP id w14mr26203855ilq.188.1626898990441; Wed, 21 Jul 2021 13:23:10 -0700 (PDT) MIME-Version: 1.0 References: <20210713220734.26302-1-niteesh.gs@gmail.com> <20210713220734.26302-4-niteesh.gs@gmail.com> In-Reply-To: From: "Niteesh G. S." Date: Thu, 22 Jul 2021 01:52:44 +0530 Message-ID: Subject: Re: [PATCH v2 3/6] python/aqmp-tui: Add AQMP TUI draft To: John Snow Content-Type: multipart/alternative; boundary="00000000000069292705c7a7f0f9" Received-SPF: pass client-ip=2607:f8b0:4864:20::130; envelope-from=niteesh.gs@gmail.com; helo=mail-il1-x130.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, HTML_MESSAGE=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Cleber Rosa , qemu-devel , Eduardo Habkost Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: "Qemu-devel" --00000000000069292705c7a7f0f9 Content-Type: text/plain; charset="UTF-8" On Wed, Jul 21, 2021 at 12:34 AM John Snow wrote: > > > On Tue, Jul 13, 2021 at 6:07 PM G S Niteesh Babu > wrote: > >> Added a draft of AQMP TUI. >> >> Implements the follwing basic features: >> 1) Command transmission/reception. >> 2) Shows events asynchronously. >> 3) Shows server status in the bottom status bar. >> >> Also added necessary pylint, mypy configurations >> >> Signed-off-by: G S Niteesh Babu >> --- >> python/qemu/aqmp/aqmp_tui.py | 332 +++++++++++++++++++++++++++++++++++ >> python/setup.cfg | 21 ++- >> 2 files changed, 352 insertions(+), 1 deletion(-) >> create mode 100644 python/qemu/aqmp/aqmp_tui.py >> >> diff --git a/python/qemu/aqmp/aqmp_tui.py b/python/qemu/aqmp/aqmp_tui.py >> new file mode 100644 >> index 0000000000..f853efc1f5 >> --- /dev/null >> +++ b/python/qemu/aqmp/aqmp_tui.py >> @@ -0,0 +1,332 @@ >> +# Copyright (c) 2021 >> +# >> +# Authors: >> +# Niteesh Babu G S >> +# >> +# This work is licensed under the terms of the GNU GPL, version 2 or >> +# later. See the COPYING file in the top-level directory. >> + >> +import argparse >> +import asyncio >> +import logging >> +from logging import Handler >> +import signal >> + >> +import urwid >> +import urwid_readline >> + >> +from .error import MultiException >> +from .protocol import ConnectError >> +from .qmp_protocol import QMP, ExecInterruptedError, ExecuteError >> +from .util import create_task, pretty_traceback >> + >> + >> +UPDATE_MSG = 'UPDATE_MSG' >> + >> +# Using root logger to enable all loggers under qemu and asyncio >> +LOGGER = logging.getLogger() >> + >> +palette = [ >> + (Token.Punctuation, '', '', '', 'h15,bold', 'g7'), >> + (Token.Text, '', '', '', '', 'g7'), >> + (Token.Name.Tag, '', '', '', 'bold,#f88', 'g7'), >> + (Token.Literal.Number.Integer, '', '', '', '#fa0', 'g7'), >> + (Token.Literal.String.Double, '', '', '', '#6f6', 'g7'), >> + (Token.Keyword.Constant, '', '', '', '#6af', 'g7'), >> + ('background', '', 'black', '', '', 'g7'), >> +] >> + >> + >> +class StatusBar(urwid.Text): >> + """ >> + A simple Text widget that currently only shows connection status. >> + """ >> + def __init__(self, text=''): >> + super().__init__(text, align='right') >> + >> + >> +class Editor(urwid_readline.ReadlineEdit): >> + """ >> + Support urwid_readline features along with >> + history support which lacks in urwid_readline >> + """ >> + def __init__(self, master): >> + super().__init__(caption='> ', multiline=True) >> + self.master = master >> + self.history = [] >> + self.last_index = -1 >> + self.show_history = False >> + >> + def keypress(self, size, key): >> + # TODO: Add some logic for down key and clean up logic if >> possible. >> + # Returning None means the key has been handled by this widget >> + # which otherwise is propogated to the parent widget to be >> + # handled >> + msg = self.get_edit_text() >> + if key == 'up' and not msg: >> + # Show the history when 'up arrow' is pressed with no input >> text. >> + # NOTE: The show_history logic is necessary because in >> 'multiline' >> + # mode (which we use) 'up arrow' is used to move between >> lines. >> + self.show_history = True >> + last_msg = self.history[self.last_index] if self.history >> else '' >> + self.set_edit_text(last_msg) >> + self.edit_pos = len(last_msg) >> + self.last_index += 1 >> + elif key == 'up' and self.show_history: >> + if self.last_index < len(self.history): >> + self.set_edit_text(self.history[self.last_index]) >> + self.edit_pos = len(self.history[self.last_index]) >> + self.last_index += 1 >> + elif key == 'meta enter': >> + # When using multiline, enter inserts a new line into the >> editor >> + # send the input to the server on alt + enter >> + self.master.cb_send_to_server(msg) >> + self.history.insert(0, msg) >> + self.set_edit_text('') >> + self.last_index = 0 >> + self.show_history = False >> + else: >> + self.show_history = False >> + self.last_index = 0 >> + return super().keypress(size, key) >> + return None >> + >> + >> +class EditorWidget(urwid.Filler): >> + """ >> + Wraps CustomEdit >> + """ >> + def __init__(self, master): >> + super().__init__(Editor(master), valign='top') >> + >> + >> +class HistoryBox(urwid.ListBox): >> + """ >> + Shows all the QMP message transmitted/received >> + """ >> + def __init__(self, master): >> + self.master = master >> + self.history = urwid.SimpleFocusListWalker([]) >> + super().__init__(self.history) >> + >> + def add_to_history(self, history): >> + self.history.append(urwid.Text(history)) >> + if self.history: >> + self.history.set_focus(len(self.history) - 1) >> + >> + >> +class HistoryWindow(urwid.Frame): >> + """ >> + Composes the HistoryBox and EditorWidget >> + """ >> + def __init__(self, master): >> + self.master = master >> + self.editor = EditorWidget(master) >> + self.editor_widget = urwid.LineBox(self.editor) >> > > It's a little confusing that "editor" is of type EditorWidget but > "editor_widget" is urwid.LineBox. > Fixed. > > >> + self.history = HistoryBox(master) >> + self.body = urwid.Pile([('weight', 80, self.history), >> + ('weight', 20, self.editor_widget)]) >> + super().__init__(self.body) >> + urwid.connect_signal(self.master, UPDATE_MSG, >> self.cb_add_to_history) >> + >> + def cb_add_to_history(self, msg, level=None): >> + formatted = [] >> + if level: >> + msg = f'[{level}]: {msg}' >> + formatted.append(msg) >> + else: >> + lexer = lexers.JsonLexer() # pylint: disable=no-member >> + for token in lexer.get_tokens(msg): >> + formatted.append(token) >> + self.history.add_to_history(formatted) >> + >> + >> +class Window(urwid.Frame): >> + """ >> + This is going to be the main window that is going to compose other >> + windows. In this stage it is unnecesssary but will be necessary in >> + future when we will have multiple windows and want to the switch >> between >> + them and display overlays >> + """ >> + def __init__(self, master): >> + self.master = master >> + footer = StatusBar() >> + body = HistoryWindow(master) >> + super().__init__(body, footer=footer) >> + >> + >> +class App(QMP): >> + def __init__(self, address): >> + urwid.register_signal(type(self), UPDATE_MSG) >> + self.window = Window(self) >> + self.address = address >> + self.aloop = None >> + self.loop = None >> + super().__init__() >> + >> + def add_to_history(self, msg, level=None): >> + urwid.emit_signal(self, UPDATE_MSG, msg, level) >> + >> + def _cb_outbound(self, msg): >> + LOGGER.debug('Request: %s', str(msg)) >> + self.add_to_history('<-- ' + str(msg)) >> + return msg >> + >> + def _cb_inbound(self, msg): >> + LOGGER.debug('Response: %s', str(msg)) >> + self.add_to_history('--> ' + str(msg)) >> + return msg >> + >> > > [DEBUG]: Response seems to duplicate the "--> {...}" incoming messages. > The debug stuff is nice to have because it gets saved to the logfile, but > is there some way to omit it from the history view, even when --debug is > on? I think we simply don't need to see the responses twice. What do you > think? > Ahh, I totally missed this. I didn't really try out the TUILogHandler much. I just made sure I was able to log all levels of messages properly. Yup, I too feel we should omit the inbound/outbound debug messages during logging inside TUI. + async def wait_for_events(self): >> + async for event in self.events: >> + self.handle_event(event) >> + >> + async def _send_to_server(self, msg): >> + # TODO: Handle more validation errors (eg: ValueError) >> + try: >> + await self._raw(bytes(msg, 'utf-8')) >> + except ExecuteError: >> + LOGGER.info('Error response from server for msg: %s', msg) >> + except ExecInterruptedError: >> + LOGGER.info('Error server disconnected before reply') >> + # FIXME: Handle this better >> + # Show the disconnected message in the history window >> + urwid.emit_signal(self, UPDATE_MSG, >> + '{"error": "Server disconnected before >> reply"}') >> + self.window.footer.set_text("Server disconnected") >> + except Exception as err: >> + LOGGER.error('Exception from _send_to_server: %s', str(err)) >> + raise err >> > > Type non-JSON or a JSON value that isn't an object will crash the whole > application. > > You need to look out for these: > > File "/home/jsnow/src/qemu/python/qemu/aqmp/qmp_protocol.py", line 479, > in _raw > msg = Message(msg) > File "/home/jsnow/src/qemu/python/qemu/aqmp/message.py", line 71, in > __init__ > self._obj = self._deserialize(self._data) > File "/home/jsnow/src/qemu/python/qemu/aqmp/message.py", line 158, in > _deserialize > raise DeserializationError(emsg, data) from err > qemu.aqmp.message.DeserializationError: Failed to deserialize QMP message. > raw bytes were: b'q\n\n' > > File "/home/jsnow/src/qemu/python/qemu/aqmp/qmp_protocol.py", line 479, > in _raw > msg = Message(msg) > File "/home/jsnow/src/qemu/python/qemu/aqmp/message.py", line 71, in > __init__ > self._obj = self._deserialize(self._data) > File "/home/jsnow/src/qemu/python/qemu/aqmp/message.py", line 160, in > _deserialize > raise UnexpectedTypeError( > qemu.aqmp.message.UnexpectedTypeError: QMP message is not a JSON object. > json value was: [] > > There's also ValueError and TypeError, but I think the way you've written > the shell here that there's not much of a chance to actually see these -- > they show up when serializing a python object, but you do bytes(msg) which > means we use the *deserialization* interface to validate user input, so you > might not actually see the other errors here ... > > Still, you theoretically could if somehow > serialize(deserialize(bytes(msg)))) raised those errors. I don't expect > that they would, but you may as well treat all of these errors the same: > the input by the user is garbage and cannot be used. No need to exit or > crash. > Will fix. > > >> + >> + def cb_send_to_server(self, msg): >> + create_task(self._send_to_server(msg)) >> + >> + def unhandled_input(self, key): >> + if key == 'esc': >> + self.kill_app() >> + >> + def kill_app(self): >> + # TODO: Work on the disconnect logic >> + create_task(self._kill_app()) >> + >> + async def _kill_app(self): >> + # It is ok to call disconnect even in disconnect state >> + try: >> + await self.disconnect() >> + LOGGER.debug('Disconnect finished. Exiting app') >> + except MultiException as err: >> + LOGGER.info('Multiple exception on disconnect: %s', str(err)) >> + # Let the app crash after providing a proper stack trace >> + raise err >> + raise urwid.ExitMainLoop() >> > > I got rid of MultiException in v2, thankfully.... ! > Nice :). > > If the server goes away, aqmp-shell shows the disconnect debug messages > well enough, but then when hitting 'esc' afterwards, we get an Exception > printed out to the terminal. Ideally, aqmp-shell should call disconnect() > as soon as it notices a problem and not only when we call _kill_app. > Will refactor. > + >> + def handle_event(self, event): >> + # FIXME: Consider all states present in qapi/run-state.json >> + if event['event'] == 'SHUTDOWN': >> + self.window.footer.set_text('Server shutdown') >> + >> + async def connect_server(self): >> + try: >> + await self.connect(self.address) >> + self.window.footer.set_text("Connected to {:s}".format( >> + f"{self.address[0]}:{self.address[1]}" >> + if isinstance(self.address, tuple) >> + else self.address >> + )) >> + except ConnectError as err: >> + LOGGER.debug('Cannot connect to server %s', str(err)) >> + self.window.footer.set_text('Server shutdown') >> + >> + def run(self, debug=False): >> + self.screen.set_terminal_properties(256) >> + >> + self.aloop = asyncio.get_event_loop() >> + self.aloop.set_debug(debug) >> + >> + # Gracefully handle SIGTERM and SIGINT signals >> + cancel_signals = [signal.SIGTERM, signal.SIGINT] >> + for sig in cancel_signals: >> + self.aloop.add_signal_handler(sig, self.kill_app) >> + >> + event_loop = urwid.AsyncioEventLoop(loop=self.aloop) >> + self.loop = urwid.MainLoop(self.window, >> + unhandled_input=self.unhandled_input, >> + handle_mouse=True, >> + event_loop=event_loop) >> + >> + create_task(self.wait_for_events(), self.aloop) >> + create_task(self.connect_server(), self.aloop) >> + try: >> + self.loop.run() >> + except Exception as err: >> + LOGGER.error('%s\n%s\n', str(err), pretty_traceback()) >> + raise err >> + >> + >> +class TUILogHandler(Handler): >> + def __init__(self, tui): >> + super().__init__() >> + self.tui = tui >> + >> + def emit(self, record): >> + level = record.levelname >> + msg = record.getMessage() >> + self.tui.add_to_history(msg, level) >> + >> + >> +def parse_address(address): >> + """ >> + This snippet was taken from qemu.qmp.__init__. >> + pylint complaints about duplicate code so it has been >> + temprorily disabled. This should be fixed once qmp is >> + replaced by aqmp in the future. >> + """ >> + components = address.split(':') >> + if len(components) == 2: >> + try: >> + port = int(components[1]) >> + except ValueError: >> + raise ValueError(f'Bad Port value in {address}') from None >> + return (components[0], port) >> + return address >> + >> > > You can just import the old QMP package and use the old function directly. > That will save you the trouble of needing to silence the duplicate code > checker too. > OK. Will change it. > + >> +def main(): >> + parser = argparse.ArgumentParser(description='AQMP TUI') >> + parser.add_argument('qmp_server', help='Address of the QMP server' >> + '< UNIX socket path | TCP addr:port >') >> + parser.add_argument('--log-file', help='The Log file name') >> + parser.add_argument('--log-level', help='Log level >> ', >> + default='debug') >> + parser.add_argument('--debug', action='store_true', >> + help='Enable debug mode for asyncio loop' >> + 'Generates lot of output, makes TUI unusable >> when' >> + 'logs are logged in the TUI itself.' >> + 'Use only when logging to a file') >> + args = parser.parse_args() >> + >> + try: >> + address = parse_address(args.qmp_server) >> + except ValueError as err: >> + parser.error(err) >> + >> + app = App(address) >> + >> > > Initializing the app can go down below the logging initialization stuff, > because the init method engages the logging module to set up the loggers, > but we want to initialize the logging paradigm ourselves before anything > touches it. > I can't move it below the initialization of loggers because TUILogHandler requires a reference to the App class. It was the simplest way I could think of to get the log messages inside the TUI. Any ideas on how can I refactor it? + if args.log_file: >> + LOGGER.addHandler(logging.FileHandler(args.log_file)) >> + else: >> + LOGGER.addHandler(TUILogHandler(app)) >> + >> + log_levels = {'debug': logging.DEBUG, >> + 'info': logging.INFO, >> + 'error': logging.ERROR} >> > > There are more log levels than just 'debug', 'info' and 'error' ... > There's probably a way to avoid having to re-write the mapping yourself. > Something in the logging module can help here. > Yes, found a way. https://github.com/python/cpython/blob/c8e35abfe304eb052a5220974006072c37d4b06a/Lib/logging/__init__.py#L119 > > >> + >> + if args.log_level not in log_levels: >> + parser.error('Invalid log level') >> + LOGGER.setLevel(log_levels[args.log_level]) >> + >> > > You can initialize the app here instead. > > >> + app.run(args.debug) >> + >> >> > I didn't pass "--debug", but I still got DEBUG messages in the history > panel. > > What I'd like to see is only WARNING messages and above in the application > unless I pass --debug, and then I want to see additional messages. > The --debug option is to enable the event loop logging and not the TUI logging. You can change the TUI logging levels using the --log-level option. Maybe I'll rename the --debug option to --asyncio-debug and set the default --log-level to WARNING. > Looking good otherwise, I think it's shaping up. Thanks! > --00000000000069292705c7a7f0f9 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
On Wed, Jul 21, 2021 at 12:34 AM John Snow <jsnow@redhat.com> wrote:
=


On T= ue, Jul 13, 2021 at 6:07 PM G S Niteesh Babu <niteesh.gs@gmail.com> wrote:
Added a draft of AQMP T= UI.

Implements the follwing basic features:
1) Command transmission/reception.
2) Shows events asynchronously.
3) Shows server status in the bottom status bar.

Also added necessary pylint, mypy configurations

Signed-off-by: G S Niteesh Babu <niteesh.gs@gmail.com>
---
=C2=A0python/qemu/aqmp/aqmp_tui.py | 332 ++++++++++++++++++++++++++++++++++= +
=C2=A0python/setup.cfg=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2= =A0 21 ++-
=C2=A02 files changed, 352 insertions(+), 1 deletion(-)
=C2=A0create mode 100644 python/qemu/aqmp/aqmp_tui.py

diff --git a/python/qemu/aqmp/aqmp_tui.py b/python/qemu/aqmp/aqmp_tui.py new file mode 100644
index 0000000000..f853efc1f5
--- /dev/null
+++ b/python/qemu/aqmp/aqmp_tui.py
@@ -0,0 +1,332 @@
+# Copyright (c) 2021
+#
+# Authors:
+#=C2=A0 Niteesh Babu G S <niteesh.gs@gmail.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2 or
+# later.=C2=A0 See the COPYING file in the top-level directory.
+
+import argparse
+import asyncio
+import logging
+from logging import Handler
+import signal
+
+import urwid
+import urwid_readline
+
+from .error import MultiException
+from .protocol import ConnectError
+from .qmp_protocol import QMP, ExecInterruptedError, ExecuteError
+from .util import create_task, pretty_traceback
+
+
+UPDATE_MSG =3D 'UPDATE_MSG'
+
+# Using root logger to enable all loggers under qemu and asyncio
+LOGGER =3D logging.getLogger()
+
+palette =3D [
+=C2=A0 =C2=A0 (Token.Punctuation, '', '', '', '= ;h15,bold', 'g7'),
+=C2=A0 =C2=A0 (Token.Text, '', '', '', '',= 'g7'),
+=C2=A0 =C2=A0 (Token.Name.Tag, '', '', '', 'bo= ld,#f88', 'g7'),
+=C2=A0 =C2=A0 (Token.Literal.Number.Integer, '', '', '= ', '#fa0', 'g7'),
+=C2=A0 =C2=A0 (Token.Literal.String.Double, '', '', '&= #39;, '#6f6', 'g7'),
+=C2=A0 =C2=A0 (Token.Keyword.Constant, '', '', '',= '#6af', 'g7'),
+=C2=A0 =C2=A0 ('background', '', 'black', '= 9;, '', 'g7'),
+]
+
+
+class StatusBar(urwid.Text):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 A simple Text widget that currently only shows connection st= atus.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, text=3D''):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(text, align=3D'right'= )
+
+
+class Editor(urwid_readline.ReadlineEdit):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Support urwid_readline features along with
+=C2=A0 =C2=A0 history support which lacks in urwid_readline
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(caption=3D'> ', mu= ltiline=3DTrue)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master =3D master
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index =3D -1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.show_history =3D False
+
+=C2=A0 =C2=A0 def keypress(self, size, key):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # TODO: Add some logic for down key and clean = up logic if possible.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # Returning None means the key has been handle= d by this widget
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # which otherwise is propogated to the parent = widget to be
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # handled
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 msg =3D self.get_edit_text()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if key =3D=3D 'up' and not msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Show the history when 'up = arrow' is pressed with no input text.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # NOTE: The show_history logic i= s necessary because in 'multiline'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # mode (which we use) 'up ar= row' is used to move between lines.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.show_history =3D True
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 last_msg =3D self.history[self.l= ast_index] if self.history else ''
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.set_edit_text(last_msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.edit_pos =3D len(last_msg)<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index +=3D 1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 elif key =3D=3D 'up' and self.show_his= tory:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.last_index < len(self= .history):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.set_edit_text= (self.history[self.last_index])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.edit_pos =3D = len(self.history[self.last_index])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index += =3D 1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 elif key =3D=3D 'meta enter':
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # When using multiline, enter in= serts a new line into the editor
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # send the input to the server o= n alt + enter
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master.cb_send_to_server(ms= g)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history.insert(0, msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.set_edit_text('') +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index =3D 0
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.show_history =3D False
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.show_history =3D False
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index =3D 0
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return super().keypress(size, ke= y)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return None
+
+
+class EditorWidget(urwid.Filler):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Wraps CustomEdit
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(Editor(master), valign=3D'= ;top')
+
+
+class HistoryBox(urwid.ListBox):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Shows all the QMP message transmitted/received
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master =3D master
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history =3D urwid.SimpleFocusListWalker([= ])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(self.history)
+
+=C2=A0 =C2=A0 def add_to_history(self, history):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history.append(urwid.Text(history))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.history:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history.set_focus(len(self.= history) - 1)
+
+
+class HistoryWindow(urwid.Frame):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Composes the HistoryBox and EditorWidget
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master =3D master
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.editor =3D EditorWidget(master)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.editor_widget =3D urwid.LineBox(self.edit= or)

It's a little confusing that &q= uot;editor" is of type EditorWidget but "editor_widget" is u= rwid.LineBox.
Fixed.=C2=A0
=C2=A0
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history =3D HistoryBox(master)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.body =3D urwid.Pile([('weight', 8= 0, self.history),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ('weight', 20, self.editor_w= idget)])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(self.body)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 urwid.connect_signal(self.master, UPDATE_MSG, = self.cb_add_to_history)
+
+=C2=A0 =C2=A0 def cb_add_to_history(self, msg, level=3DNone):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if level:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 msg =3D f'[{level}]: {msg}&#= 39;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted.append(msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lexer =3D lexers.JsonLexer()=C2= =A0 # pylint: disable=3Dno-member
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for token in lexer.get_tokens(ms= g):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted.append(t= oken)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.history.add_to_history(formatted)
+
+
+class Window(urwid.Frame):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 This is going to be the main window that is going to compose= other
+=C2=A0 =C2=A0 windows. In this stage it is unnecesssary but will be necess= ary in
+=C2=A0 =C2=A0 future when we will have multiple windows and want to the sw= itch between
+=C2=A0 =C2=A0 them and display overlays
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master =3D master
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 footer =3D StatusBar()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 body =3D HistoryWindow(master)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__(body, footer=3Dfooter)
+
+
+class App(QMP):
+=C2=A0 =C2=A0 def __init__(self, address):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 urwid.register_signal(type(self), UPDATE_MSG)<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window =3D Window(self)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.address =3D address
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.aloop =3D None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.loop =3D None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__()
+
+=C2=A0 =C2=A0 def add_to_history(self, msg, level=3DNone):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 urwid.emit_signal(self, UPDATE_MSG, msg, level= )
+
+=C2=A0 =C2=A0 def _cb_outbound(self, msg):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.debug('Request: %s', str(msg))<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_to_history('<-- ' + str(ms= g))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return msg
+
+=C2=A0 =C2=A0 def _cb_inbound(self, msg):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.debug('Response: %s', str(msg))=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_to_history('--> ' + str(ms= g))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return msg
+

[DEBUG]: Response seems to duplicate = the "--> {...}" incoming messages.
The debug stuff is nice to have because it gets = saved to the logfile, but is there some way to omit it from the history vie= w, even when --debug is on? I think we simply don't need to see the res= ponses twice. What do you think?
Ahh, I totally missed this.= I didn't really try out the TUILogHandler much. I just made sure I was=
able to log al= l levels of messages properly.
Yup, I too feel we should omit the inbound/outbound debug m= essages during=C2=A0logging inside TUI.

+=C2=A0 =C2=A0 = async def wait_for_events(self):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 async for event in self.events:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.handle_event(event)
+
+=C2=A0 =C2=A0 async def _send_to_server(self, msg):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # TODO: Handle more validation errors (eg: Val= ueError)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 await self._raw(bytes(msg, '= utf-8'))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ExecuteError:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.info('Error response = from server for msg: %s', msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ExecInterruptedError:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.info('Error server di= sconnected before reply')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # FIXME: Handle this better
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Show the disconnected message = in the history window
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 urwid.emit_signal(self, UPDATE_M= SG,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 '{"error": "Server disco= nnected before reply"}')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window.footer.set_text(&quo= t;Server disconnected")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.error('Exception from= _send_to_server: %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise err
<= br>
Type non-JSON or a JSON value that isn't an object will crash the who= le application.

You need to look out for these:

=C2=A0 File "/home/jsnow/src/qemu/python/qemu/a= qmp/qmp_protocol.py", line 479, in _raw
=C2=A0 =C2=A0 msg =3D Messa= ge(msg)
=C2=A0 File "/home/jsnow/src/qemu/python/qemu/aqmp/message.= py", line 71, in __init__
=C2=A0 =C2=A0 self._obj =3D self._deseria= lize(self._data)
=C2=A0 File "/home/jsnow/src/qemu/python/qemu/aqmp= /message.py", line 158, in _deserialize
=C2=A0 =C2=A0 raise Deseria= lizationError(emsg, data) from err
qemu.aqmp.message.DeserializationErro= r: Failed to deserialize QMP message.
=C2=A0 raw bytes were: b'q\n\n= '

=C2=A0 File "/home/jsnow/src/qemu/pytho= n/qemu/aqmp/qmp_protocol.py", line 479, in _raw
=C2=A0 =C2=A0 msg = =3D Message(msg)
=C2=A0 File "/home/jsnow/src/qemu/python/qemu/aqmp= /message.py", line 71, in __init__
=C2=A0 =C2=A0 self._obj =3D self= ._deserialize(self._data)
=C2=A0 File "/home/jsnow/src/qemu/python/= qemu/aqmp/message.py", line 160, in _deserialize
=C2=A0 =C2=A0 rais= e UnexpectedTypeError(
qemu.aqmp.message.UnexpectedTypeError: QMP messag= e is not a JSON object.
=C2=A0 json value was: []

There's also ValueError and TypeError, but I think the way you'v= e written the shell here that there's not much of a chance to actually = see these -- they show up when serializing a python object, but you do byte= s(msg) which means we use the *deserialization* interface to validate user = input, so you might not actually see the other errors here ...
Still, you theoretically could if somehow serialize(deserializ= e(bytes(msg)))) raised those errors. I don't expect that they would, bu= t you may as well treat all of these errors the same: the input by the user= is garbage and cannot be used. No need to exit or crash.
Will fix.=C2=A0
=C2=A0
+
+=C2=A0 =C2=A0 def cb_send_to_server(self, msg):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self._send_to_server(msg))
+
+=C2=A0 =C2=A0 def unhandled_input(self, key):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if key =3D=3D 'esc':
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.kill_app()
+
+=C2=A0 =C2=A0 def kill_app(self):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # TODO: Work on the disconnect logic
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self._kill_app())
+
+=C2=A0 =C2=A0 async def _kill_app(self):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # It is ok to call disconnect even in disconne= ct state
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 await self.disconnect()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.debug('Disconnect fin= ished. Exiting app')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except MultiException as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.info('Multiple except= ion on disconnect: %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Let the app crash after provid= ing a proper stack trace
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise err
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 raise urwid.ExitMainLoop()

I got rid of MultiException in v2, thankfully.... !
<= /blockquote>
Nice :).
<= div class=3D"gmail_quote">

If the server goes away, aqmp-= shell shows the disconnect debug messages well enough, but then when hittin= g 'esc' afterwards, we get an Exception printed out to the terminal= . Ideally, aqmp-shell should call disconnect() as soon as it notices a prob= lem and not only when we call _kill_app.
Will refactor.=C2=A0
+
+=C2=A0 =C2=A0 def handle_event(self, event):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # FIXME: Consider all states present in qapi/r= un-state.json
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if event['event'] =3D=3D 'SHUTDOWN= ':
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window.footer.set_text('= ;Server shutdown')
+
+=C2=A0 =C2=A0 async def connect_server(self):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 await self.connect(self.address)=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window.footer.set_text(&quo= t;Connected to {:s}".format(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{self.addre= ss[0]}:{self.address[1]}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if isinstance(self= .address, tuple)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else self.address<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ConnectError as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.debug('Cannot connect= to server %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window.footer.set_text('= ;Server shutdown')
+
+=C2=A0 =C2=A0 def run(self, debug=3DFalse):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.screen.set_terminal_properties(256)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.aloop =3D asyncio.get_event_loop()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.aloop.set_debug(debug)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # Gracefully handle SIGTERM and SIGINT signals=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 cancel_signals =3D [signal.SIGTERM, signal.SIG= INT]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for sig in cancel_signals:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.aloop.add_signal_handler(si= g, self.kill_app)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 event_loop =3D urwid.AsyncioEventLoop(loop=3Ds= elf.aloop)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.loop =3D urwid.MainLoop(self.window,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0unhandled_input=3Dself.= unhandled_input,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0handle_mouse=3DTrue, +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0event_loop=3Devent_loop= )
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self.wait_for_events(), self.aloop= )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self.connect_server(), self.aloop)=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.loop.run()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.error('%s\n%s\n',= str(err), pretty_traceback())
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise err
+
+
+class TUILogHandler(Handler):
+=C2=A0 =C2=A0 def __init__(self, tui):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.tui =3D tui
+
+=C2=A0 =C2=A0 def emit(self, record):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 level =3D record.levelname
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 msg =3D record.getMessage()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.tui.add_to_history(msg, level)
+
+
+def parse_address(address):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 This snippet was taken from qemu.qmp.__init__.
+=C2=A0 =C2=A0 pylint complaints about duplicate code so it has been
+=C2=A0 =C2=A0 temprorily disabled. This should be fixed once qmp is
+=C2=A0 =C2=A0 replaced by aqmp in the future.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 components =3D address.split(':')
+=C2=A0 =C2=A0 if len(components) =3D=3D 2:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D int(components[1])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ValueError:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueError(f'Bad Port = value in {address}') from None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return (components[0], port)
+=C2=A0 =C2=A0 return address
+

You can just import the old QMP packa= ge and use the old function directly. That will save you the trouble of nee= ding to silence the duplicate code checker too.
OK. Will ch= ange it.=C2=A0
+
+def main():
+=C2=A0 =C2=A0 parser =3D argparse.ArgumentParser(description=3D'AQMP T= UI')
+=C2=A0 =C2=A0 parser.add_argument('qmp_server', help=3D'Addres= s of the QMP server'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 '< UNIX socket path | TCP addr:port >')
+=C2=A0 =C2=A0 parser.add_argument('--log-file', help=3D'The Lo= g file name')
+=C2=A0 =C2=A0 parser.add_argument('--log-level', help=3D'Log l= evel <debug|info|error>',
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 default=3D'debug')
+=C2=A0 =C2=A0 parser.add_argument('--debug', action=3D'store_t= rue',
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 help=3D'Enable debug mode for asyncio loop'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 'Generates lot of output, makes TUI unusable when'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 'logs are logged in the TUI itself.'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 'Use only when logging to a file')
+=C2=A0 =C2=A0 args =3D parser.parse_args()
+
+=C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 address =3D parse_address(args.qmp_server)
+=C2=A0 =C2=A0 except ValueError as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 parser.error(err)
+
+=C2=A0 =C2=A0 app =3D App(address)
+

Initializing the app can go down belo= w the logging initialization stuff, because the init method engages the log= ging module to set up the loggers, but we want to initialize the logging pa= radigm ourselves before anything touches it.
=
I can't move it = below the initialization of loggers because TUILogHandler requires a refere= nce to the App class.
It was the simplest=C2=A0way I could think of to get the log message= s inside the TUI. Any ideas on how can I refactor it?

+=C2=A0 =C2=A0 if args.log_file:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.addHandler(logging.FileHandler(args.log= _file))
+=C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 LOGGER.addHandler(TUILogHandler(app))
+
+=C2=A0 =C2=A0 log_levels =3D {'debug': logging.DEBUG,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 'info&#= 39;: logging.INFO,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 'error&= #39;: logging.ERROR}

There are more log= levels than just 'debug', 'info' and 'error' ... T= here's probably a way to avoid having to re-write the mapping yourself.= Something in the logging module can help here.
Yes, found a way.=
=C2=A0
+
+=C2=A0 =C2=A0 if args.log_level not in log_levels:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 parser.error('Invalid log level')
+=C2=A0 =C2=A0 LOGGER.setLevel(log_levels[args.log_level])
+

You can initialize the app here inste= ad.
=C2=A0
+=C2=A0 =C2=A0 app.run(args.debug)
+


=C2=A0I didn't pass "--debug&= quot;, but I still got DEBUG messages in the history panel.
=C2=A0
What I'd like t= o see is only WARNING messages and above in the application unless I pass -= -debug, and then I want to see additional messages.
=C2=A0
The --debug option is to enable the event loop logging and not= the TUI logging.
You can change th= e TUI logging levels using the --log-level option.
Maybe I'll rename the --debug option to --asynci= o-debug and set the default
= --log-level to WARNING.
=C2=A0
Looking good otherwise, I think it's shaping up. Tha= nks!
--00000000000069292705c7a7f0f9--