File 076-console-Cleanup-and-improve-console-menu-handling.patch of Package virt-manager

Subject: console: Cleanup and improve console menu handling
From: Cole Robinson crobinso@redhat.com Sat Mar 2 16:22:53 2024 -0500
Date: Sun Mar 3 12:52:37 2024 -0500:
Git: 014d1ef99b6bf1bd34050914fc3485395d6dd9ad

- Rework the ConsolePages abstraction to carry the Gtk.Menu. makes
  it all less messy

- Make the console menu the single source of truth for console
  embeddability, and error message reporting

- Small misc cleanups here and there

Signed-off-by: Cole Robinson <crobinso@redhat.com>

diff --git a/tests/uitests/test_details.py b/tests/uitests/test_details.py
index b873004d..1e8362f2 100644
--- a/tests/uitests/test_details.py
+++ b/tests/uitests/test_details.py
@@ -839,7 +839,7 @@ def testDetailsConsoleChecksSSH(app):
     # Check initial state
     _checkcon("Graphical console not configured")
     _stop()
-    _check_textconsole_menu("No graphical console available")
+    _check_textconsole_menu("Graphical console not configured")
 
     # Add a SDL graphics device which can't be displayed
     detailsbtn.click()
diff --git a/virtManager/details/console.py b/virtManager/details/console.py
index b3616bfb..00ed002b 100644
--- a/virtManager/details/console.py
+++ b/virtManager/details/console.py
@@ -188,22 +188,37 @@ class vmmOverlayToolbar:
         self.timed_revealer = None
 
 
-class _ConsoleMenu:
+def _cant_embed_graphics(ginfo):
+    if ginfo.gtype in ["vnc", "spice"]:
+        return
+
+    msg = _("Cannot display graphical console type '%s'") % ginfo.gtype
+    return msg
+
+
+class _ConsoleMenu(vmmGObject):
     """
     Helper class for building the text/graphical console menu list
     """
+    def __init__(self, show_cb, toggled_cb):
+        vmmGObject.__init__(self)
+        self._menu = Gtk.Menu()
+        self._menu.connect("show", show_cb)
+        self._toggled_cb = toggled_cb
+
+    def _cleanup(self):
+        self._menu.destroy()
+        self._menu = None
+        self._toggled_cb = None
+
 
     ################
     # Internal API #
     ################
 
     def _build_serial_menu_items(self, vm):
-        devs = vmmSerialConsole.get_serialcon_devices(vm)
-        if len(devs) == 0:
-            return [[_("No text console available"), None, None]]
-
         ret = []
-        for dev in devs:
+        for dev in vmmSerialConsole.get_serialcon_devices(vm):
             if dev.DEVICE_TYPE == "console":
                 label = _("Text Console %d") % (dev.get_xml_idx() + 1)
             else:
@@ -211,48 +226,58 @@ class _ConsoleMenu:
 
             tooltip = vmmSerialConsole.can_connect(vm, dev)
             ret.append([label, dev, tooltip])
+
+        if not ret:
+            ret = [[_("No text console available"), None, None]]
         return ret
 
     def _build_graphical_menu_items(self, vm):
-        devs = vm.xmlobj.devices.graphics
-        if len(devs) == 0:
-            return [[_("No graphical console available"), None, None]]
 
         from ..device.gfxdetails import vmmGraphicsDetails
 
         ret = []
-        preferred = None
-        for idx, dev in enumerate(devs):
-            label = (_("Graphical Console") + " " +
-                     vmmGraphicsDetails.graphics_pretty_type_simple(dev.type))
+        found_default = False
+        for gdev in vm.xmlobj.devices.graphics:
+            idx = gdev.get_xml_idx()
+            ginfo = ConnectionInfo(vm.conn, gdev)
 
+            label = (_("Graphical Console") + " " +
+                     vmmGraphicsDetails.graphics_pretty_type_simple(gdev.type))
             if idx > 0:
                 label += " %s" % (idx + 1)
 
-            tooltip = None
-            if dev.type not in self.embeddable_graphics():
-                tooltip = _("virt-manager cannot display graphical "
-                            "console type '%s'") % (dev.type)
-            elif preferred is not None:
-                tooltip = _("virt-manager does not support more "
-                            "than one graphical console")
-            else:
-                preferred = idx
+            tooltip = _cant_embed_graphics(ginfo)
+            if not tooltip:
+                if not found_default:
+                    found_default = True
+                else:
+                    tooltip = _("virt-manager does not support more "
+                                "than one graphical console")
 
-            ret.append([label, dev, tooltip])
+            ret.append([label, ginfo, tooltip])
+
+        if not ret:
+            ret = [[_("Graphical console not configured for guest"),
+                    None, None]]
         return ret
 
+    def _get_selected_menu_item(self):
+        for child in self._menu.get_children():
+            if hasattr(child, 'get_active') and child.get_active():
+                return child
+
 
     ##############
     # Public API #
     ##############
 
-    def rebuild_menu(self, vm, submenu, toggled_cb):
-        oldlabel = None
-        for child in submenu.get_children():
-            if hasattr(child, 'get_active') and child.get_active():
-                oldlabel = child.get_label()
-            submenu.remove(child)
+    def rebuild_menu(self, vm):
+        olditem = self._get_selected_menu_item()
+        oldlabel = olditem and olditem.get_label() or None
+
+        # Clear menu
+        for child in self._menu.get_children():
+            self._menu.remove(child)
 
         graphics = self._build_graphical_menu_items(vm)
         serials = self._build_serial_menu_items(vm)
@@ -263,12 +288,12 @@ class _ConsoleMenu:
         last_item = None
         for (label, dev, tooltip) in items:
             if label is None:
-                submenu.add(Gtk.SeparatorMenuItem())
+                self._menu.add(Gtk.SeparatorMenuItem())
                 continue
 
-            cb = toggled_cb
-            cbdata = dev
             sensitive = bool(dev and not tooltip)
+            if not sensitive and not tooltip:
+                tooltip = label
 
             active = False
             if oldlabel is None and sensitive:
@@ -285,25 +310,30 @@ class _ConsoleMenu:
 
             item.set_label(label)
             item.set_active(active and sensitive)
-            if cbdata and sensitive:
-                item.connect("toggled", cb, cbdata)
-
             item.set_sensitive(sensitive)
             item.set_tooltip_text(tooltip or None)
-            submenu.add(item)
+            item.vmm_data = dev
+            if sensitive:
+                item.connect("toggled", self._toggled_cb)
+            self._menu.add(item)
 
-        submenu.show_all()
+        self._menu.show_all()
 
-    def activate_default(self, menu):
-        for child in menu.get_children():
+    def activate_default(self):
+        for child in self._menu.get_children():
             if child.get_sensitive() and hasattr(child, "toggled"):
                 child.toggled()
                 return True
         return False
 
-    def embeddable_graphics(self):
-        ret = ["vnc", "spice"]
-        return ret
+    def get_selected(self):
+        row = self._get_selected_menu_item()
+        if not row:
+            row = self._menu.get_children()[0]
+        return row.get_label(), row.vmm_data, row.get_tooltip_text()
+
+    def get_menu(self):
+        return self._menu
 
 
 class vmmConsolePages(vmmGObjectUI):
@@ -336,9 +366,6 @@ class vmmConsolePages(vmmGObjectUI):
 
         # Fullscreen toolbar
         self._keycombo_menu = build_keycombo_menu(self._do_send_key)
-        self._console_list_menu = Gtk.Menu()
-        self._console_list_menu.connect("show",
-                self._populate_console_list_menu)
 
         self._overlay_toolbar_fullscreen = vmmOverlayToolbar(
             on_leave_fn=self._leave_fullscreen,
@@ -358,7 +385,9 @@ class vmmConsolePages(vmmGObjectUI):
         self.widget("serial-pages").set_show_tabs(False)
         self.widget("console-gfx-pages").set_show_tabs(False)
 
-        self._consolemenu = _ConsoleMenu()
+        self._consolemenu = _ConsoleMenu(
+                self._on_console_menu_show_cb,
+                self._on_console_menu_toggled_cb)
         self._serial_consoles = []
 
         # Signals are added by vmmVMWindow. Don't use connect_signals here
@@ -390,6 +419,9 @@ class vmmConsolePages(vmmGObjectUI):
             serial.cleanup()
         self._serial_consoles = []
 
+        self._consolemenu.cleanup()
+        self._consolemenu = None
+
 
     #################
     # Internal APIs #
@@ -689,38 +721,13 @@ class vmmConsolePages(vmmGObjectUI):
     # Viewer login attempts #
     #########################
 
-    def _init_viewer(self):
+    def _init_viewer(self, ginfo, errmsg):
         if self._viewer or not self.is_visible():
-            # Don't try and login for these cases
             return
 
-        ginfo = None
-        try:
-            gdevs = self.vm.xmlobj.devices.graphics
-            for idx, dev in enumerate(gdevs):
-                if dev.type in self._consolemenu.embeddable_graphics():
-                    ginfo = ConnectionInfo(self.vm.conn, gdevs[idx], idx)
-                    break
-        except Exception as e:  # pragma: no cover
-            # We can fail here if VM is destroyed: xen is a bit racy
-            # and can't handle domain lookups that soon after
-            log.exception("Getting graphics console failed: %s", str(e))
-            return
-
-        if ginfo is None:
-            log.debug("No graphics configured for guest")
-            self._activate_gfx_unavailable_page(
-                _("Graphical console not configured for guest"))
-            return
-
-        if ginfo.gtype not in self._consolemenu.embeddable_graphics():
-            log.debug("Don't know how to show graphics type '%s' "
-                          "disabling console page", ginfo.gtype)
-
-            msg = (_("Cannot display graphical console type '%s'")
-                     % ginfo.gtype)
-
-            self._activate_gfx_unavailable_page(msg)
+        if errmsg:
+            log.debug("No acceptable graphics to connect to")
+            self._activate_gfx_unavailable_page(errmsg)
             return
 
         if (not self.vm.get_console_autoconnect() and
@@ -736,6 +743,9 @@ class vmmConsolePages(vmmGObjectUI):
             if ginfo.gtype == "vnc":
                 viewer_class = VNCViewer
             elif ginfo.gtype == "spice":
+                # We do this here and not in the embed check, since user
+                # is probably expecting their spice console to work, so we
+                # should show an explicit failure
                 if SPICE_GTK_IMPORT_ERROR:
                     raise RuntimeError(
                             "Error opening SPICE console: %s" %
@@ -893,16 +903,18 @@ class vmmConsolePages(vmmGObjectUI):
     # Console list menu handling #
     ##############################
 
-    def _console_list_menu_toggled(self, src, dev):
-        if not dev or dev.DEVICE_TYPE == "graphics":
+    def _console_menu_view_selected(self):
+        name, dev, errmsg = self._consolemenu.get_selected()
+        is_graphics = hasattr(dev, "gtype")
+
+        if errmsg or not dev or is_graphics:
             self.widget("console-pages").set_current_page(
                     _CONSOLE_PAGE_GRAPHICS)
-            self.idle_add(self._init_viewer)
+            self.idle_add(self._init_viewer, dev, errmsg)
             return
 
         target_port = dev.get_xml_idx()
         serial = None
-        name = src.get_label()
         for s in self._serial_consoles:
             if s.name == name:
                 serial = s
@@ -922,20 +934,18 @@ class vmmConsolePages(vmmGObjectUI):
         self.widget("console-pages").set_current_page(_CONSOLE_PAGE_SERIAL)
         self.widget("serial-pages").set_current_page(page_idx)
 
-    def _populate_console_list_menu(self, ignore=None):
-        self._consolemenu.rebuild_menu(
-                self.vm, self._console_list_menu,
-                self._console_list_menu_toggled)
+    def _populate_console_menu(self):
+        self._consolemenu.rebuild_menu(self.vm)
 
     def _toggle_first_console_menu_item(self):
         # We iterate through the 'console' menu and activate the first
         # valid entry... hacky but it works
-        self._populate_console_list_menu()
-        found = self._consolemenu.activate_default(self._console_list_menu)
+        self._populate_console_menu()
+        found = self._consolemenu.activate_default()
         if not found:
             # Calling this with dev=None will trigger _init_viewer
             # which shows some meaningful errors
-            self._console_list_menu_toggled(None, None)
+            self._console_menu_view_selected()
 
     def _activate_default_console_page(self):
         if self.vm.is_runable():
@@ -954,6 +964,12 @@ class vmmConsolePages(vmmGObjectUI):
         # just started, so connect to the default page
         self._toggle_first_console_menu_item()
 
+    def _on_console_menu_toggled_cb(self, src):
+        self._console_menu_view_selected()
+
+    def _on_console_menu_show_cb(self, src):
+        self._populate_console_menu()
+
 
     ################
     # UI listeners #
@@ -964,7 +980,7 @@ class vmmConsolePages(vmmGObjectUI):
 
     def _connect_button_clicked_cb(self, src):
         self._viewer_connect_clicked = True
-        self._init_viewer()
+        self._console_menu_view_selected()
 
     def _page_changed_cb(self, src, origpage, newpage):
         # Hide the contents of all other pages, so they don't screw
@@ -1009,7 +1025,7 @@ class vmmConsolePages(vmmGObjectUI):
     def vmwindow_get_keycombo_menu(self):
         return self._keycombo_menu
     def vmwindow_get_console_list_menu(self):
-        return self._console_list_menu
+        return self._consolemenu.get_menu()
     def vmwindow_get_viewer_is_visible(self):
         return self._viewer_is_visible()
     def vmwindow_get_can_usb_redirect(self):
diff --git a/virtManager/details/sshtunnels.py b/virtManager/details/sshtunnels.py
index f2b37f5a..f3463cae 100644
--- a/virtManager/details/sshtunnels.py
+++ b/virtManager/details/sshtunnels.py
@@ -20,9 +20,9 @@ class ConnectionInfo(object):
     """
     Holds all the bits needed to make a connection to a graphical console
     """
-    def __init__(self, conn, gdev, gidx):
-        self.gidx  = gidx
+    def __init__(self, conn, gdev):
         self.gtype = gdev.type
+        self.gidx = gdev.get_xml_idx()
         self.gport = str(gdev.port) if gdev.port else None
         self.gsocket = (gdev.listens and gdev.listens[0].socket) or gdev.socket
         self.gaddr = gdev.listen or "127.0.0.1"
openSUSE Build Service is sponsored by