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.5 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 6E683C4338F for ; Sat, 21 Aug 2021 22:23:30 +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 7F84C6113B for ; Sat, 21 Aug 2021 22:23:29 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.1 mail.kernel.org 7F84C6113B Authentication-Results: mail.kernel.org; dmarc=fail (p=none dis=none) header.from=gmail.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=nongnu.org Received: from localhost ([::1]:37886 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mHZOi-0004GZ-5p for qemu-devel@archiver.kernel.org; Sat, 21 Aug 2021 18:23:28 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:36786) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mHZNi-0003YV-5v for qemu-devel@nongnu.org; Sat, 21 Aug 2021 18:22:26 -0400 Received: from mail-pg1-x535.google.com ([2607:f8b0:4864:20::535]:33689) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1mHZNd-0007ct-Cb for qemu-devel@nongnu.org; Sat, 21 Aug 2021 18:22:25 -0400 Received: by mail-pg1-x535.google.com with SMTP id c17so12879524pgc.0 for ; Sat, 21 Aug 2021 15:22:21 -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=w70G3bAlQdcXG9CzZFOi1dFzF9DuqYYw1pnBuhnyt8Q=; b=vHe4cVVjlX+8DfAs5Yz2Gy0ydRcFa7hMEsGbwf7x6YsXvHS5ZE66NQDO3w0b0xyXZW Ud+rd6ulJyuf4Ff4wQ+NIErC3cDJoDJZ1kOXcTx87iG9OoAt46+QQ0Jm1fb7zXcczvt9 5IB8sJw9H+J2AuQr1NSoceUic+a5Cci7PK8qQgZho5OzBxZx59uTm4OBv6Sow9EPjzlo kRuL0V+uUrjobpooMIGEN7yD/LQxScZzz5689UCrzu12sKDwryKzp4iaWm7cpCu+e6ch HzYx+ANY5y9ukE4rtVpfYxSNfqAVng3dvCK2rJAOtuXenYugHer/0namr9I7uzcTptIU nqtw== 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=w70G3bAlQdcXG9CzZFOi1dFzF9DuqYYw1pnBuhnyt8Q=; b=N+KaO1zcokFULrlGhR8mZaLmM1QvX+TeJlXKcjIB9ZPH+DFyGiuCTNZLFTdyw0NEkU bfvJXQK7d+Oqd3ljB/2UR/N9fQaQV4jZe4W8t/W9wYL9NwKZcC1njf/2HutfXc2jrxOL t8jD+YPwZl+kKkRRRN2YOAPd8Ey6+f7W8Ii6c0GKHvV23s8cPUa9q1VeWlHzrHrlX9qE 652Hr1fZyWo2NT+aZ3LLtkZrREXWHYW28i2ZuzMHYIkXa6Swkg6uX8dfoZflBZf1yBXR iFKqTPAYAOUZFwRFNd88rNXity0FeXQxsAdFKeEc69zaFX9I2MW37pot+1tjPzP/VA7n W61A== X-Gm-Message-State: AOAM53016mZKuofGVLHWIJcL5vxDxMGQrCL7RX5xmACeHFr7ncbifrTl oyWt6w4wDmiJvdRn3+OzIYD4vYERgGiIg/mHxBw= X-Google-Smtp-Source: ABdhPJxl7r+Yz1lg2kdddi3KMMJgWY6P2VT22PVDxm/etU2zLPAvkBvyxNAYSsCc5VEJtLl120yDyzgyvYE8BoKWb24= X-Received: by 2002:a65:6a09:: with SMTP id m9mr24893360pgu.269.1629584539907; Sat, 21 Aug 2021 15:22:19 -0700 (PDT) MIME-Version: 1.0 References: <20210819173831.23515-1-niteesh.gs@gmail.com> <20210819173831.23515-4-niteesh.gs@gmail.com> In-Reply-To: From: "Niteesh G. S." Date: Sun, 22 Aug 2021 03:51:53 +0530 Message-ID: Subject: Re: [PATCH v4 3/7] python/aqmp-tui: Add AQMP TUI draft To: John Snow Content-Type: multipart/alternative; boundary="000000000000a1fc5e05ca19377c" Received-SPF: pass client-ip=2607:f8b0:4864:20::535; envelope-from=niteesh.gs@gmail.com; helo=mail-pg1-x535.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: Eduardo Habkost , Kashyap Chamarthy , Markus Armbruster , Wainer Moschetta , qemu-devel , Stefan Hajnoczi , Cleber Rosa , Eric Blake Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: "Qemu-devel" --000000000000a1fc5e05ca19377c Content-Type: text/plain; charset="UTF-8" On Sun, Aug 22, 2021 at 1:36 AM John Snow wrote: > On Thu, Aug 19, 2021 at 1:39 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 type annotations and necessary pylint, >> mypy configurations >> >> Signed-off-by: G S Niteesh Babu >> --- >> python/qemu/aqmp/aqmp_tui.py | 566 +++++++++++++++++++++++++++++++++++ >> python/setup.cfg | 15 +- >> 2 files changed, 579 insertions(+), 2 deletions(-) >> 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..12c9c4162a >> --- /dev/null >> +++ b/python/qemu/aqmp/aqmp_tui.py >> @@ -0,0 +1,566 @@ >> +# 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. >> +""" >> +AQMP TUI >> + >> +AQMP TUI is an asynchronous interface built on top the of the AQMP >> library. >> +It is the successor of QMP-shell and is bought-in as a replacement for >> it. >> + >> +Example Usage: aqmp-tui >> +Full Usage: aqmp-tui --help >> +""" >> + >> +import argparse >> +import asyncio >> +import logging >> +from logging import Handler, LogRecord >> +import signal >> +from typing import ( >> + List, >> + Optional, >> + Tuple, >> + Type, >> + Union, >> + cast, >> +) >> + >> +import urwid >> +import urwid_readline >> + >> +from ..qmp import QEMUMonitorProtocol, QMPBadPortError >> +from .message import DeserializationError, Message, UnexpectedTypeError >> +from .protocol import ConnectError >> +from .qmp_client import ExecInterruptedError, QMPClient >> +from .util import create_task, pretty_traceback >> + >> + >> +# The name of the signal that is used to update the history list >> +UPDATE_MSG: str = 'UPDATE_MSG' >> + >> + >> +def format_json(msg: str) -> str: >> + """ >> + Formats given multi-line JSON message into a single-line message. >> + Converting into single line is more asthetically pleasing when >> looking >> + along with error messages. >> + >> + Eg: >> + Input: >> + [ 1, >> + true, >> + 3 ] >> + The above input is not a valid QMP message and produces the >> following error >> + "QMP message is not a JSON object." >> + When displaying this in TUI in multiline mode we get >> + >> + [ 1, >> + true, >> + 3 ]: QMP message is not a JSON object. >> + >> + whereas in singleline mode we get the following >> + >> + [1, true, 3]: QMP message is not a JSON object. >> + >> + The single line mode is more asthetically pleasing. >> + >> + :param msg: >> + The message to formatted into single line. >> + >> + :return: Formatted singleline message. >> + >> + NOTE: We cannot use the JSON module here because it is only capable >> of >> + format valid JSON messages. But here the goal is to also format >> invalid >> + JSON messages. >> + """ >> + msg = msg.replace('\n', '') >> + words = msg.split(' ') >> + words = [word for word in words if word != ''] >> + return ' '.join(words) >> + >> + >> +def has_tui_handler(logger: logging.Logger, >> + handler_type: Type[Handler]) -> bool: >> + """ >> + The Logger class has no interface to check if a certain type of >> handler is >> + installed or not. So we provide an interface to do so. >> + >> + :param logger: >> + Logger object >> + :param handler_type: >> + The type of the handler to be checked. >> + >> + :return: returns True if handler of type `handler_type` is installed >> else >> + False. >> + """ >> + handlers = logger.handlers >> + for handler in handlers: >> + if isinstance(handler, handler_type): >> + return True >> + return False >> + >> + >> +class App(QMPClient): >> + """ >> + Implements the AQMP TUI. >> + >> + Initializes the widgets and starts the urwid event loop. >> + """ >> + def __init__(self, address: Union[str, Tuple[str, int]]) -> None: >> + """ >> + Initializes the TUI. >> + >> + :param address: >> + Address of the server to connect to. >> + """ >> + urwid.register_signal(type(self), UPDATE_MSG) >> + self.window = Window(self) >> + self.address = address >> + self.aloop: Optional[asyncio.AbstractEventLoop] = None >> + super().__init__() >> + >> + def add_to_history(self, msg: str, level: Optional[str] = None) -> >> None: >> + """ >> + Appends the msg to the history list. >> + >> + :param msg: >> + The raw message to be appended in string type. >> + """ >> + urwid.emit_signal(self, UPDATE_MSG, msg, level) >> + >> + def _cb_outbound(self, msg: Message) -> Message: >> + """ >> + Callback: outbound message hook. >> + >> + Appends the outgoing messages to the history box. >> + >> + :param msg: raw outbound message. >> + :return: final outbound message. >> + """ >> + str_msg = str(msg) >> + >> + if not has_tui_handler(logging.getLogger(), TUILogHandler): >> + logging.debug('Request: %s', str_msg) >> + self.add_to_history('<-- ' + str_msg) >> + return msg >> + >> + def _cb_inbound(self, msg: Message) -> Message: >> + """ >> + Callback: outbound message hook. >> + >> + Appends the incoming messages to the history box. >> + >> + :param msg: raw inbound message. >> + :return: final inbound message. >> + """ >> + str_msg = str(msg) >> + >> + if not has_tui_handler(logging.getLogger(), TUILogHandler): >> + logging.debug('Request: %s', str_msg) >> + self.add_to_history('--> ' + str_msg) >> + return msg >> + >> > > Starting here: ... > > >> + def handle_event(self, event: Message) -> None: >> + """ >> + Handles the event. >> + >> + :param event: >> + The event to be handled. >> + """ >> + # TODO: Consider all states present in qapi/run-state.json >> + if event['event'] == 'SHUTDOWN': >> + self._set_status('[Server Shutdown]') >> + >> + async def wait_for_events(self) -> None: >> + """ >> + This coroutine continously waits for events and dispatches them. >> + """ >> + async for event in self.events: >> + self.handle_event(event) >> + >> > > ... until here, we can remove these from this series, because we don't use > them for anything by the end of this series. We can re-add them once that > "TODO" is done. > I have removed this in the upcoming revision. > > >> + async def _send_to_server(self, msg: Message) -> None: >> + """ >> + This coroutine sends the message to the server. >> + The message has to be pre-validated. >> + >> + :param msg: >> + Pre-validated message to be to sent to the server. >> + >> + :raise Exception: When an unhandled exception is caught. >> + """ >> + try: >> + await self._raw(msg, assign_id='id' not in msg) >> + except ExecInterruptedError: >> + logging.info('Error server disconnected before reply') >> + self.add_to_history('Server disconnected before reply', >> 'ERROR') >> + self._set_status("[Server Disconnected]") >> + except Exception as err: >> + logging.error('Exception from _send_to_server: %s', str(err)) >> + raise err >> + >> + def cb_send_to_server(self, raw_msg: str) -> None: >> + """ >> + Validates and sends the message to the server. >> + The raw string message is first converted into a Message object >> + and is then sent to the server. >> + >> + :param raw_msg: >> + The raw string message to be sent to the server. >> + >> + :raise Exception: When an unhandled exception is caught. >> + """ >> + try: >> + raw_msg = format_json(raw_msg) >> + msg = Message(bytes(raw_msg, encoding='utf-8')) >> + create_task(self._send_to_server(msg)) >> + except (ValueError, TypeError) as err: >> + logging.info('Invalid message: %s', str(err)) >> + self.add_to_history(f'{raw_msg}: {err}') >> + except (DeserializationError, UnexpectedTypeError) as err: >> + logging.info('Invalid message: %s', err.error_message) >> + self.add_to_history(f'{raw_msg}: {err.error_message}') >> + >> + def unhandled_input(self, key: str) -> None: >> + """ >> + Handle's keys which haven't been handled by the child widgets. >> + >> + :param key: >> + Unhandled key >> + """ >> + if key == 'esc': >> + self.kill_app() >> + >> + def kill_app(self) -> None: >> + """ >> + Initiates killing of app. A bridge between asynchronous and >> synchronous >> + code. >> + """ >> + create_task(self._kill_app()) >> + >> + async def _kill_app(self) -> None: >> + """ >> + This coroutine initiates the actual disconnect process and calls >> + urwid.ExitMainLoop() to kill the TUI. >> + >> + :raise Exception: When an unhandled exception is caught. >> + """ >> + # It is ok to call disconnect even in disconnect state >> + try: >> + await self.disconnect() >> + logging.debug('Disconnect finished. Exiting app') >> + except EOFError: >> + # We receive an EOF during disconnect, ignore that >> + pass >> + except Exception as err: >> + logging.info('_kill_app: %s', str(err)) >> + # Let the app crash after providing a proper stack trace >> + raise err >> + raise urwid.ExitMainLoop() >> + >> + def _set_status(self, msg: str) -> None: >> + """ >> + Sets the message as the status. >> + >> + :param msg: >> + The message to be displayed in the status bar. >> + """ >> + self.window.footer.set_text(msg) >> + >> + def _get_formatted_address(self) -> str: >> + """ >> + Returns a formatted version of the server's address. >> + >> + :return: formatted address >> + """ >> + if isinstance(self.address, tuple): >> + host, port = self.address >> + addr = f'{host}:{port}' >> + else: >> + addr = f'{self.address}' >> + return addr >> + >> + async def connect_server(self) -> None: >> + """ >> + Initiates a connection to the server at address `self.address` >> + and in case of a failure, sets the status to the respective >> error. >> + """ >> + try: >> + await self.connect(self.address) >> + addr = self._get_formatted_address() >> + self._set_status(f'Connected to {addr}') >> + except ConnectError as err: >> + logging.info('connect_server: ConnectError %s', str(err)) >> + self._set_status(f'[ConnectError: {err.error_message}]') >> + >> + def run(self, debug: bool = False) -> None: >> + """ >> + Starts the long running co-routines and the urwid event loop. >> + >> + :param debug: >> + Enables/Disables asyncio event loop debugging >> + """ >> + 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) >> + main_loop = urwid.MainLoop(urwid.AttrMap(self.window, >> 'background'), >> + unhandled_input=self.unhandled_input, >> + handle_mouse=True, >> + event_loop=event_loop) >> + >> + create_task(self.wait_for_events(), self.aloop) >> > > This can be removed since it isn't used for anything by the end of this > series. We can re-add the event watcher when the status updater is actually > completed. > Removed. > > >> + create_task(self.connect_server(), self.aloop) >> + try: >> + main_loop.run() >> + except Exception as err: >> + logging.error('%s\n%s\n', str(err), pretty_traceback()) >> + raise err >> + >> + >> +class StatusBar(urwid.Text): >> + """ >> + A simple statusbar modelled using the Text widget. The status can be >> + set using the set_text function. All text set is aligned to right. >> + """ >> + def __init__(self, text: str = ''): >> + super().__init__(text, align='right') >> + >> + >> +class Editor(urwid_readline.ReadlineEdit): >> + """ >> + A simple editor modelled using the urwid_readline.ReadlineEdit >> widget. >> + Mimcs GNU readline shortcuts and provides history support. >> + >> + The readline shortcuts can be found below: >> + https://github.com/rr-/urwid_readline#features >> + >> + Along with the readline features, this editor also has support for >> + history. Pressing the 'up' arrow key with empty message box, lists >> the >> + previous message inplace. >> + >> + Currently there is no support to save the history to a file. The >> history of >> + previous commands is lost on exit. >> + """ >> + def __init__(self, master: App) -> None: >> + """ >> + Initializes the editor widget >> + >> + :param master: Reference to the TUI object. >> + """ >> + super().__init__(caption='> ', multiline=True) >> + self.master = master >> + self.history: List[str] = [] >> + self.last_index: int = -1 >> + self.show_history: bool = False >> + >> + def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]: >> + """ >> + Handles the keypress on this widget. >> + >> + :param size: >> + The current size of the widget. >> + :param key: >> + The key to be handled. >> + >> + :return: Unhandled key if any. >> + """ >> + # TODO: Add some logic for down key and clean up logic if >> possible. >> > > What logic needs to be added here? Can this comment be made more explicit? > What kind of cleanup do we need to do here, still? > (Is now the right time to do that cleanup, or not?) > > If you want to leave the TODO in, then edit setup.cfg and add the > exemption for it. > I have cleaned the logic as much as I can and have also added support for the down key to go up the history stack. So this TODO will be removed in the upcoming revision. > > >> + # 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 cast(Optional[str], super().keypress(size, key)) >> + return None >> + >> + >> +class EditorWidget(urwid.Filler): >> + """ >> + The Editor is a flow widget and has to wrapped inside a box widget. >> + This class wraps the Editor inside filler widget. >> + """ >> + def __init__(self, master: App) -> None: >> + super().__init__(Editor(master), valign='top') >> + >> + >> +class HistoryBox(urwid.ListBox): >> + """ >> + This widget is modelled using the ListBox widget, contains the list >> of >> + all messages both QMP messages and log messsages to be shown in the >> TUI. >> + >> + The messages are urwid.Text widgets. On every append of a message, >> the >> + focus is shifted to the last appended message. >> + """ >> + def __init__(self, master: App) -> None: >> + """ >> + Initializes the historybox widget >> + >> + :param master: Reference to the TUI object. >> + """ >> + self.master = master >> + self.history = urwid.SimpleFocusListWalker([]) >> + super().__init__(self.history) >> + >> + def add_to_history(self, history: str) -> None: >> + """ >> + Appends a message to the list and set the focus to the last >> appended >> + message. >> + >> + :param history: >> + The history item(message/event) to be appended to the list. >> + """ >> + self.history.append(urwid.Text(history)) >> + if self.history: >> + self.history.set_focus(len(self.history) - 1) >> + >> + >> +class HistoryWindow(urwid.Frame): >> + """ >> + This window composes the HistoryBox and EditorWidget in a horizontal >> split. >> + By default the first focus is given to the history box. >> + """ >> + def __init__(self, master: App) -> None: >> + """ >> + Initializes this widget and its child widgets. >> + >> + :param master: Reference to the TUI object. >> + """ >> + self.master = master >> + self.editor_widget = EditorWidget(master) >> + self.editor = urwid.LineBox(self.editor_widget) >> + self.history = HistoryBox(master) >> + self.body = urwid.Pile([('weight', 80, self.history), >> + ('weight', 20, self.editor)]) >> + super().__init__(self.body) >> + urwid.connect_signal(self.master, UPDATE_MSG, >> self.cb_add_to_history) >> + >> + def cb_add_to_history(self, msg: str, level: Optional[str] = None) >> -> None: >> + """ >> + Appends a message to the history box >> + >> + :param msg: >> + The message to be appended to the history box. >> + """ >> + if level: >> + msg = f'[{level}]: {msg}' >> + self.history.add_to_history(msg) >> + >> + >> +class Window(urwid.Frame): >> + """ >> + This window is the top most widget of the TUI and will contain other >> + windows. Each window is responsible for displaying a specific >> + functionality. >> + For eg: The history window is responsible for showing the history of >> + messages and the editor. >> + """ >> + def __init__(self, master: App) -> None: >> + """ >> + Initializes this widget and its child windows. >> + >> + :param master: Reference to the TUI object. >> + """ >> + self.master = master >> + footer = StatusBar() >> + body = HistoryWindow(master) >> + super().__init__(body, footer=footer) >> + >> + >> +class TUILogHandler(Handler): >> + """ >> + This handler routes all the log messages to the TUI screen. >> + It is installed to the root logger to so that the log message from >> all >> + libraries begin used is routed to the screen. >> + """ >> + def __init__(self, tui: App) -> None: >> + """ >> + Initializes the handler class. >> + >> + :param tui: >> + Reference to the TUI object. >> + """ >> + super().__init__() >> + self.tui = tui >> + >> + def emit(self, record: LogRecord) -> None: >> + """ >> + Emits a record to the TUI screen. >> + >> + Appends the log message to the TUI screen >> + """ >> + level = record.levelname >> + msg = record.getMessage() >> + self.tui.add_to_history(msg, level) >> + >> + >> +def main() -> None: >> + """ >> + Driver of the whole script, parses arguments, initialize the TUI and >> + the logger. >> + """ >> + parser = argparse.ArgumentParser(description='AQMP TUI') >> + parser.add_argument('qmp_server', help='Address of the QMP server. ' >> + 'Format ') >> + parser.add_argument('--log-file', help='The Log file name') >> + parser.add_argument('--log-level', default='WARNING', >> + help='Log level >> ') >> + parser.add_argument('--asyncio-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. ' >> + 'Use only when logging to a file.') >> + args = parser.parse_args() >> + >> + try: >> + address = QEMUMonitorProtocol.parse_address(args.qmp_server) >> + except QMPBadPortError as err: >> + parser.error(str(err)) >> + >> + app = App(address) >> + >> + root_logger = logging.getLogger() >> + root_logger.setLevel(logging.getLevelName(args.log_level)) >> + >> + if args.log_file: >> + root_logger.addHandler(logging.FileHandler(args.log_file)) >> + else: >> + root_logger.addHandler(TUILogHandler(app)) >> + >> + app.run(args.asyncio_debug) >> + >> + >> +if __name__ == '__main__': >> + main() >> diff --git a/python/setup.cfg b/python/setup.cfg >> index a0ed3279d8..1ff2b907a2 100644 >> --- a/python/setup.cfg >> +++ b/python/setup.cfg >> @@ -81,8 +81,19 @@ namespace_packages = True >> # fusepy has no type stubs: >> allow_subclassing_any = True >> >> +[mypy-qemu.aqmp.aqmp_tui] >> +# urwid and urwid_readline have no type stubs: >> +allow_subclassing_any = True >> + >> +# The following missing import directives are because these libraries do >> not >> +# provide type stubs. Allow them on an as-needed basis for mypy. >> [mypy-fuse] >> -# fusepy has no type stubs: >> +ignore_missing_imports = True >> + >> +[mypy-urwid] >> +ignore_missing_imports = True >> + >> +[mypy-urwid_readline] >> ignore_missing_imports = True >> >> [pylint.messages control] >> @@ -97,7 +108,7 @@ ignore_missing_imports = True >> # --disable=W". >> disable=too-many-function-args, # mypy handles this with less false >> positives. >> no-member, # mypy also handles this better. >> - missing-docstring, # FIXME >> + # missing-docstring, # FIXME >> > > ^ Once patch #1 is removed, this stuff should also go. > Removed. > > >> fixme, # FIXME >> >> [pylint.basic] >> -- >> 2.17.1 >> > > --000000000000a1fc5e05ca19377c Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


On Sun, Aug 22, 2021 at 1:36 AM John Snow <jsnow@redhat.com> wrote:
On Thu, Aug= 19, 2021 at 1:39 PM G S Niteesh Babu <niteesh.gs@gmail.com> 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 type annotations and necessary pylint,
mypy configurations

Signed-off-by: G S Niteesh Babu <niteesh.gs@gmail.com>
---
=C2=A0python/qemu/aqmp/aqmp_tui.py | 566 ++++++++++++++++++++++++++++++++++= +
=C2=A0python/setup.cfg=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2= =A0 15 +-
=C2=A02 files changed, 579 insertions(+), 2 deletions(-)
=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..12c9c4162a
--- /dev/null
+++ b/python/qemu/aqmp/aqmp_tui.py
@@ -0,0 +1,566 @@
+# 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.
+"""
+AQMP TUI
+
+AQMP TUI is an asynchronous interface built on top the of the AQMP library= .
+It is the successor of QMP-shell and is bought-in as a replacement for it.=
+
+Example Usage: aqmp-tui <SOCKET | TCP IP:PORT>
+Full Usage: aqmp-tui --help
+"""
+
+import argparse
+import asyncio
+import logging
+from logging import Handler, LogRecord
+import signal
+from typing import (
+=C2=A0 =C2=A0 List,
+=C2=A0 =C2=A0 Optional,
+=C2=A0 =C2=A0 Tuple,
+=C2=A0 =C2=A0 Type,
+=C2=A0 =C2=A0 Union,
+=C2=A0 =C2=A0 cast,
+)
+
+import urwid
+import urwid_readline
+
+from ..qmp import QEMUMonitorProtocol, QMPBadPortError
+from .message import DeserializationError, Message, UnexpectedTypeError +from .protocol import ConnectError
+from .qmp_client import ExecInterruptedError, QMPClient
+from .util import create_task, pretty_traceback
+
+
+# The name of the signal that is used to update the history list
+UPDATE_MSG: str =3D 'UPDATE_MSG'
+
+
+def format_json(msg: str) -> str:
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Formats given multi-line JSON message into a single-line mes= sage.
+=C2=A0 =C2=A0 Converting into single line is more asthetically pleasing wh= en looking
+=C2=A0 =C2=A0 along with error messages.
+
+=C2=A0 =C2=A0 Eg:
+=C2=A0 =C2=A0 Input:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [ 1,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 true,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 3 ]
+=C2=A0 =C2=A0 The above input is not a valid QMP message and produces the = following error
+=C2=A0 =C2=A0 "QMP message is not a JSON object."
+=C2=A0 =C2=A0 When displaying this in TUI in multiline mode we get
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 [ 1,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 true,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 3 ]: QMP message is not a JSON object.<= br> +
+=C2=A0 =C2=A0 whereas in singleline mode we get the following
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 [1, true, 3]: QMP message is not a JSON object= .
+
+=C2=A0 =C2=A0 The single line mode is more asthetically pleasing.
+
+=C2=A0 =C2=A0 :param msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The message to formatted into single line.
+
+=C2=A0 =C2=A0 :return: Formatted singleline message.
+
+=C2=A0 =C2=A0 NOTE: We cannot use the JSON module here because it is only = capable of
+=C2=A0 =C2=A0 format valid JSON messages. But here the goal is to also for= mat invalid
+=C2=A0 =C2=A0 JSON messages.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 msg =3D msg.replace('\n', '')
+=C2=A0 =C2=A0 words =3D msg.split(' ')
+=C2=A0 =C2=A0 words =3D [word for word in words if word !=3D ''] +=C2=A0 =C2=A0 return ' '.join(words)
+
+
+def has_tui_handler(logger: logging.Logger,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hand= ler_type: Type[Handler]) -> bool:
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 The Logger class has no interface to check if a certain type= of handler is
+=C2=A0 =C2=A0 installed or not. So we provide an interface to do so.
+
+=C2=A0 =C2=A0 :param logger:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Logger object
+=C2=A0 =C2=A0 :param handler_type:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The type of the handler to be checked.
+
+=C2=A0 =C2=A0 :return: returns True if handler of type `handler_type` is i= nstalled else
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0False.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 handlers =3D logger.handlers
+=C2=A0 =C2=A0 for handler in handlers:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if isinstance(handler, handler_type):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
+=C2=A0 =C2=A0 return False
+
+
+class App(QMPClient):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Implements the AQMP TUI.
+
+=C2=A0 =C2=A0 Initializes the widgets and starts the urwid event loop.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, address: Union[str, Tuple[str, int]]) -&g= t; None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes the TUI.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param address:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Address of the server to connect= to.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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: Optional[asyncio.AbstractEventLoop= ] =3D None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 super().__init__()
+
+=C2=A0 =C2=A0 def add_to_history(self, msg: str, level: Optional[str] =3D = None) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends the msg to the history list.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The raw message to be appended i= n string type.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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: Message) -> Message:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Callback: outbound message hook.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends the outgoing messages to the history b= ox.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg: raw outbound message.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: final outbound message.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 str_msg =3D str(msg)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not has_tui_handler(logging.getLogger(), TU= ILogHandler):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.debug('Request: %s&#= 39;, 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
+
+=C2=A0 =C2=A0 def _cb_inbound(self, msg: Message) -> Message:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Callback: outbound message hook.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends the incoming messages to the history b= ox.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg: raw inbound message.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: final inbound message.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 str_msg =3D str(msg)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not has_tui_handler(logging.getLogger(), TU= ILogHandler):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.debug('Request: %s&#= 39;, 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
+

Starting here: ...
=C2= =A0
+=C2=A0 =C2=A0 def handle_event(self, event: Message) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Handles the event.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param event:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The event to be handled.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # TODO: Consider all states present in qapi/ru= n-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._set_status('[Server Sh= utdown]')
+
+=C2=A0 =C2=A0 async def wait_for_events(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This coroutine continously waits for events an= d dispatches them.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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)
+

... until here, we can remove these f= rom this series, because we don't use them for anything by the end of t= his series. We can re-add them once that "TODO" is done.
I have removed this in the upcoming revision.=C2=A0
=C2=A0
+=C2=A0 =C2=A0 async def _send_to_server(self, msg: Message) -> None: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This coroutine sends the message to the server= .
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The message has to be pre-validated.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Pre-validated message to be to s= ent to the server.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :raise Exception: When an unhandled exception = is caught.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 await self._raw(msg, assign_id= =3D'id' not in msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ExecInterruptedError:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.info('Error server dis= connected before reply')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_to_history('Server = disconnected before reply', 'ERROR')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._set_status("[Server D= isconnected]")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.error('Exception fro= m _send_to_server: %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise err
+
+=C2=A0 =C2=A0 def cb_send_to_server(self, raw_msg: str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Validates and sends the message to the server.=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 The raw string message is first converted into= a Message object
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 and is then sent to the server.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param raw_msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The raw string message to be sen= t to the server.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :raise Exception: When an unhandled exception = is caught.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raw_msg =3D format_json(raw_msg)=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 msg =3D Message(bytes(raw_msg, e= ncoding=3D'utf-8'))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self._send_to_server= (msg))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (ValueError, TypeError) as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.info('Invalid message:= %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_to_history(f'{raw_m= sg}: {err}')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (DeserializationError, UnexpectedTypeEr= ror) as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.info('Invalid message:= %s', err.error_message)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.add_to_history(f'{raw_m= sg}: {err.error_message}')
+
+=C2=A0 =C2=A0 def unhandled_input(self, key: str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Handle's keys which haven't been handl= ed by the child widgets.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param key:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Unhandled key
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initiates killing of app. A bridge between asy= nchronous and synchronous
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 code.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 create_task(self._kill_app())
+
+=C2=A0 =C2=A0 async def _kill_app(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 This coroutine initiates the actual disconnect= process and calls
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 urwid.ExitMainLoop() to kill the TUI.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :raise Exception: When an unhandled exception = is caught.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 logging.debug('Disconnect fi= nished. Exiting app')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except EOFError:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # We receive an EOF during disco= nnect, ignore that
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.info('_kill_app: %s= 9;, 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()
+
+=C2=A0 =C2=A0 def _set_status(self, msg: str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Sets the message as the status.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The message to be displayed in t= he status bar.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.window.footer.set_text(msg)
+
+=C2=A0 =C2=A0 def _get_formatted_address(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns a formatted version of the server'= s address.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: formatted address
+=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 host, port =3D self.address
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 addr =3D f'{host}:{port}'= ;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 addr =3D f'{self.address}= 9;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return addr
+
+=C2=A0 =C2=A0 async def connect_server(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initiates a connection to the server at addres= s `self.address`
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 and in case of a failure, sets the status to t= he respective error.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 addr =3D self._get_formatted_add= ress()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._set_status(f'Connected= to {addr}')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except ConnectError as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logging.info('connect_server: = ConnectError %s', str(err))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._set_status(f'[ConnectE= rror: {err.error_message}]')
+
+=C2=A0 =C2=A0 def run(self, debug: bool =3D False) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Starts the long running co-routines and the ur= wid event loop.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param debug:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Enables/Disables asyncio event l= oop debugging
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 main_loop =3D urwid.MainLoop(urwid.AttrMap(sel= f.window, 'background'),
+=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= )

This can be removed since it isn'= t used for anything by the end of this series. We can re-add the event watc= her when the status updater is actually completed.
Remo= ved.=C2=A0
=C2=A0
+=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 main_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 logging.error('%s\n%s\n'= , str(err), pretty_traceback())
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise err
+
+
+class StatusBar(urwid.Text):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 A simple statusbar modelled using the Text widget. The statu= s can be
+=C2=A0 =C2=A0 set using the set_text function. All text set is aligned to = right.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, text: str =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 A simple editor modelled using the urwid_readline.ReadlineEd= it widget.
+=C2=A0 =C2=A0 Mimcs GNU readline shortcuts and provides history support. +
+=C2=A0 =C2=A0 The readline shortcuts can be found below:
+=C2=A0 =C2=A0 https://github.com/rr-/urwid_readline#f= eatures
+
+=C2=A0 =C2=A0 Along with the readline features, this editor also has suppo= rt for
+=C2=A0 =C2=A0 history. Pressing the 'up' arrow key with empty mess= age box, lists the
+=C2=A0 =C2=A0 previous message inplace.
+
+=C2=A0 =C2=A0 Currently there is no support to save the history to a file.= The history of
+=C2=A0 =C2=A0 previous commands is lost on exit.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master: App) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes the editor widget
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param master: Reference to the TUI object. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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: List[str] =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.last_index: int =3D -1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.show_history: bool =3D False
+
+=C2=A0 =C2=A0 def keypress(self, size: Tuple[int, int], key: str) -> Op= tional[str]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Handles the keypress on this widget.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param size:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The current size of the widget.<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param key:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The key to be handled.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: Unhandled key if any.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # TODO: Add some logic for down key and clean = up logic if possible.

What logic needs = to be added here? Can this comment be made more explicit? What kind of clea= nup do we need to do here, still?
(Is now the right time to do th= at cleanup, or not?)

If you want to leave the TODO in, th= en edit setup.cfg and add the exemption for it.
I have = cleaned the logic as much as I can and have also added support for the down= key to go up the history stack.
So this TODO will be removed in the upcoming= revision.
=C2=A0
+=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 cast(Optional[str], super= ().keypress(size, key))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return None
+
+
+class EditorWidget(urwid.Filler):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 The Editor is a flow widget and has to wrapped inside a box = widget.
+=C2=A0 =C2=A0 This class wraps the Editor inside filler widget.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master: App) -> None:
+=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 This widget is modelled using the ListBox widget, contains t= he list of
+=C2=A0 =C2=A0 all messages both QMP messages and log messsages to be shown= in the TUI.
+
+=C2=A0 =C2=A0 The messages are urwid.Text widgets. On every append of a me= ssage, the
+=C2=A0 =C2=A0 focus is shifted to the last appended message.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master: App) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes the historybox widget
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param master: Reference to the TUI object. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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: str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends a message to the list and set the focu= s to the last appended
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 message.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param history:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The history item(message/event) = to be appended to the list.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 This window composes the HistoryBox and EditorWidget in a ho= rizontal split.
+=C2=A0 =C2=A0 By default the first focus is given to the history box.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master: App) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes this widget and its child widgets.=
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param master: Reference to the TUI object. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.master =3D master
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.editor_widget =3D EditorWidget(master) +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.editor =3D urwid.LineBox(self.editor_widg= et)
+=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)]= )
+=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: str, level: Optional[str] = =3D None) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends a message to the history box
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param msg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The message to be appended to th= e history box.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 self.history.add_to_history(msg)
+
+
+class Window(urwid.Frame):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 This window is the top most widget of the TUI and will conta= in other
+=C2=A0 =C2=A0 windows. Each window is responsible for displaying a specifi= c
+=C2=A0 =C2=A0 functionality.
+=C2=A0 =C2=A0 For eg: The history window is responsible for showing the hi= story of
+=C2=A0 =C2=A0 messages and the editor.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, master: App) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes this widget and its child windows.=
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param master: Reference to the TUI object. +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 TUILogHandler(Handler):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 This handler routes all the log messages to the TUI screen.<= br> +=C2=A0 =C2=A0 It is installed to the root logger to so that the log messag= e from all
+=C2=A0 =C2=A0 libraries begin used is routed to the screen.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 def __init__(self, tui: App) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Initializes the handler class.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param tui:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Reference to the TUI object.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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: LogRecord) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Emits a record to the TUI screen.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Appends the log message to the TUI screen
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=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 main() -> None:
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Driver of the whole script, parses arguments, initialize the= TUI and
+=C2=A0 =C2=A0 the logger.
+=C2=A0 =C2=A0 """
+=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 'Format <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', default=3D'WA= RNING',
+=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'Log level <CRITICAL|ERROR|WARNING|INFO|DEBUG|>= ')
+=C2=A0 =C2=A0 parser.add_argument('--asyncio-debug', action=3D'= ;store_true',
+=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. '
+=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 QEMUMonitorProtocol.parse_address(= args.qmp_server)
+=C2=A0 =C2=A0 except QMPBadPortError as err:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 parser.error(str(err))
+
+=C2=A0 =C2=A0 app =3D App(address)
+
+=C2=A0 =C2=A0 root_logger =3D logging.getLogger()
+=C2=A0 =C2=A0 root_logger.setLevel(logging.getLevelName(args.log_level)) +
+=C2=A0 =C2=A0 if args.log_file:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 root_logger.addHandler(logging.FileHandler(arg= s.log_file))
+=C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 root_logger.addHandler(TUILogHandler(app))
+
+=C2=A0 =C2=A0 app.run(args.asyncio_debug)
+
+
+if __name__ =3D=3D '__main__':
+=C2=A0 =C2=A0 main()
diff --git a/python/setup.cfg b/python/setup.cfg
index a0ed3279d8..1ff2b907a2 100644
--- a/python/setup.cfg
+++ b/python/setup.cfg
@@ -81,8 +81,19 @@ namespace_packages =3D True
=C2=A0# fusepy has no type stubs:
=C2=A0allow_subclassing_any =3D True

+[mypy-qemu.aqmp.aqmp_tui]
+# urwid and urwid_readline have no type stubs:
+allow_subclassing_any =3D True
+
+# The following missing import directives are because these libraries do n= ot
+# provide type stubs. Allow them on an as-needed basis for mypy.
=C2=A0[mypy-fuse]
-# fusepy has no type stubs:
+ignore_missing_imports =3D True
+
+[mypy-urwid]
+ignore_missing_imports =3D True
+
+[mypy-urwid_readline]
=C2=A0ignore_missing_imports =3D True

=C2=A0[pylint.messages control]
@@ -97,7 +108,7 @@ ignore_missing_imports =3D True
=C2=A0# --disable=3DW".
=C2=A0disable=3Dtoo-many-function-args,=C2=A0 # mypy handles this with less= false positives.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0no-member,=C2=A0 # mypy also handles this= better.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 missing-docstring, # FIXME
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # missing-docstring, # FIXME
<= div>
^ Once patch #1 is removed, this stuff should also go.
Removed.=C2=A0
=C2=A0
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0fixme, # FIXME

=C2=A0[pylint.basic]
--
2.17.1
=C2=A0
--000000000000a1fc5e05ca19377c--