# Copyright (C) 2006, 2013-2014 Red Hat, Inc. # Copyright (C) 2006 Daniel P. Berrange # # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. import logging import queue import threading from gi.repository import Gio from gi.repository import GLib from gi.repository import Gtk from . import packageutils from .baseclass import vmmGObject from .connect import vmmConnect from .connmanager import vmmConnectionManager from .inspection import vmmInspection from .systray import vmmSystray DETAILS_PERF = 1 DETAILS_CONFIG = 2 DETAILS_CONSOLE = 3 (PRIO_HIGH, PRIO_LOW) = range(1, 3) def _show_startup_error(fn): """ Decorator to show a modal error dialog if an exception is raised from a startup routine """ # pylint: disable=protected-access def newfn(self, *args, **kwargs): try: return fn(self, *args, **kwargs) except Exception as e: modal = self._can_exit() self.err.show_err(str(e), modal=modal) self._exit_app_if_no_windows() return newfn class vmmEngine(vmmGObject): CLI_SHOW_MANAGER = "manager" CLI_SHOW_DOMAIN_CREATOR = "creator" CLI_SHOW_DOMAIN_EDITOR = "editor" CLI_SHOW_DOMAIN_PERFORMANCE = "performance" CLI_SHOW_DOMAIN_CONSOLE = "console" CLI_SHOW_HOST_SUMMARY = "summary" @classmethod def get_instance(cls): if not cls._instance: cls._instance = vmmEngine() return cls._instance __gsignals__ = { "app-closing": (vmmGObject.RUN_FIRST, None, []), } def __init__(self): vmmGObject.__init__(self) self._exiting = False self._window_count = 0 self._gtkapplication = None self._init_gtk_application() self._timer = None self._tick_counter = 0 self._tick_thread_slow = False self._tick_thread = threading.Thread(name="Tick thread", target=self._handle_tick_queue, args=()) self._tick_thread.daemon = True self._tick_queue = queue.PriorityQueue(100) @property def _connobjs(self): return vmmConnectionManager.get_instance().conns def _cleanup(self): if self._timer is not None: GLib.source_remove(self._timer) ################# # init handling # ################# def _default_startup(self, skip_autostart, cliuri): """ Actual startup routines if we are running a new instance of the app """ vmmSystray.get_instance() vmmInspection.get_instance() self.add_gsettings_handle( self.config.on_stats_update_interval_changed( self._timer_changed_cb)) self._schedule_timer() self._tick_thread.start() self._tick() uris = list(self._connobjs.keys()) if not uris: logging.debug("No stored URIs found.") else: logging.debug("Loading stored URIs:\n%s", " \n".join(sorted(uris))) if not skip_autostart: self.idle_add(self._autostart_conns) if not self.config.get_conn_uris() and not cliuri: # Only add default if no connections are currently known self.timeout_add(1000, self._add_default_conn) def _add_default_conn(self): """ If there's no cached connections, or any requested on the command line, try to determine a default URI and open it, possibly talking to packagekit and other bits """ manager = self._get_manager() # Manager fail message msg = _("Could not detect a default hypervisor. Make\n" "sure the appropriate virtualization packages\n" "containing kvm, qemu, libvirt, etc. are\n" "installed, and that libvirtd is running.\n\n" "A hypervisor connection can be manually\n" "added via File->Add Connection") logging.debug("Determining default libvirt URI") packages_verified = False try: libvirt_packages = self.config.libvirt_packages packages = self.config.hv_packages + libvirt_packages packages_verified = packageutils.check_packagekit( manager, manager.err, packages) except Exception: logging.exception("Error talking to PackageKit") tryuri = None if packages_verified: tryuri = "qemu:///system" elif not self.config.test_first_run: tryuri = vmmConnect.default_uri() if tryuri is None: manager.set_startup_error(msg) return # packagekit API via gnome-software doesn't even work nicely these # days. Not sure what the state of this warning is... # # warnmsg = _("The 'libvirtd' service will need to be started.\n\n" # "After that, virt-manager will connect to libvirt on\n" # "the next application start up.") # if not connected and not libvirtd_started: # manager.err.ok(_("Libvirt service must be started"), warnmsg) def idle_connect(): def _open_completed(c, ConnectError): if ConnectError: self._handle_conn_error(c, ConnectError) packageutils.start_libvirtd() conn = vmmConnectionManager.get_instance().add_conn(tryuri) conn.set_autoconnect(True) conn.connect_once("open-completed", _open_completed) conn.open() self.idle_add(idle_connect) def _autostart_conns(self): """ We serialize conn autostart, so polkit/ssh-askpass doesn't spam """ if self._exiting: return connections_queue = queue.Queue() auto_conns = [conn.get_uri() for conn in self._connobjs.values() if conn.get_autoconnect()] def add_next_to_queue(): if not auto_conns: connections_queue.put(None) else: connections_queue.put(auto_conns.pop(0)) def conn_open_completed(_conn, ConnectError): # Explicitly ignore connection errors, we've done that # for a while and it can be noisy if ConnectError is not None: logging.debug("Autostart connection error: %s", ConnectError.details) add_next_to_queue() def handle_queue(): while True: uri = connections_queue.get() if uri is None: return if self._exiting: return if uri not in self._connobjs: add_next_to_queue() continue conn = self._connobjs[uri] conn.connect_once("open-completed", conn_open_completed) self.idle_add(conn.open) add_next_to_queue() self._start_thread(handle_queue, "Conn autostart thread") ############################ # Gtk Application handling # ############################ def _on_gtk_application_activated(self, ignore): """ Invoked after application.run() """ if not self._application.get_windows(): logging.debug("Initial gtkapplication activated") self._application.add_window(Gtk.Window()) def _init_gtk_application(self): self._application = Gtk.Application( application_id="org.virt-manager.virt-manager", flags=0) self._application.register(None) self._application.connect("activate", self._on_gtk_application_activated) action = Gio.SimpleAction.new("cli_command", GLib.VariantType.new("(sss)")) action.connect("activate", self._handle_cli_command) self._application.add_action(action) def start(self, uri, show_window, domain, skip_autostart): """ Public entrypoint from virt-manager cli. If app is already running, connect to it and exit, otherwise run our functional default startup. """ # Dispatch dbus CLI command if uri and not show_window: show_window = self.CLI_SHOW_MANAGER data = GLib.Variant("(sss)", (uri or "", show_window or "", domain or "")) is_remote = self._application.get_is_remote() if not is_remote: self._default_startup(skip_autostart, uri) self._application.activate_action("cli_command", data) if is_remote: logging.debug("Connected to remote app instance.") return self._application.run(None) ########################### # timer and tick handling # ########################### def _timer_changed_cb(self, *args, **kwargs): ignore1 = args ignore2 = kwargs self._schedule_timer() def _schedule_timer(self): interval = self.config.get_stats_update_interval() * 1000 if self._timer is not None: self.remove_gobject_timeout(self._timer) self._timer = None self._timer = self.timeout_add(interval, self._tick) def _add_obj_to_tick_queue(self, obj, isprio, **kwargs): if self._tick_queue.full(): if not self._tick_thread_slow: logging.debug("Tick is slow, not running at requested rate.") self._tick_thread_slow = True return self._tick_counter += 1 self._tick_queue.put((isprio and PRIO_HIGH or PRIO_LOW, self._tick_counter, obj, kwargs)) def schedule_priority_tick(self, conn, kwargs): # Called directly from connection self._add_obj_to_tick_queue(conn, True, **kwargs) def _tick(self): for conn in self._connobjs.values(): self._add_obj_to_tick_queue(conn, False, stats_update=True, pollvm=True) return 1 def _handle_tick_queue(self): while True: ignore1, ignore2, conn, kwargs = self._tick_queue.get() try: conn.tick_from_engine(**kwargs) except Exception: # Don't attempt to show any UI error here, since it # can cause dialogs to appear from nowhere if say # libvirtd is shut down logging.debug("Error polling connection %s", conn.get_uri(), exc_info=True) self._tick_queue.task_done() return 1 ##################################### # window counting and exit handling # ##################################### def increment_window_counter(self): """ Public function, called by toplevel windows """ self._window_count += 1 logging.debug("window counter incremented to %s", self._window_count) def decrement_window_counter(self): """ Public function, called by toplevel windows """ self._window_count -= 1 logging.debug("window counter decremented to %s", self._window_count) self._exit_app_if_no_windows() def _systray_is_embedded(self): """ We don't use window tracking here: systray isn't a window and even when 'show' has been requested it may not be embedded in a visible tray area, so we have to check it separately. """ return vmmSystray.get_instance().is_embedded() def _can_exit(self): return (self._window_count <= 0 and not self._systray_is_embedded()) def _exit_app_if_no_windows(self): if self._exiting: return if self._can_exit(): logging.debug("No windows found, requesting app exit") self.exit_app() def exit_app(self): """ Public call, manager/details/... use this to force exit the app """ if self._exiting: return self._exiting = True def _do_exit(): try: vmmConnectionManager.get_instance().cleanup() self.emit("app-closing") self.cleanup() if self.config.test_leak_debug: objs = self.config.get_objects() # Engine will always appear to leak objs.remove(self.object_key) for name in objs: logging.debug("LEAK: %s", name) logging.debug("Exiting app normally.") finally: self._application.quit() # We stick this in an idle callback, so the exit_app() caller # reference is dropped, and leak check debug doesn't give a # false positive self.idle_add(_do_exit) ########################################## # Window launchers from virt-manager cli # ########################################## def _find_vm_by_cli_str(self, uri, clistr): """ Lookup a VM by a string passed in on the CLI. Can be either ID, domain name, or UUID """ if clistr.isdigit(): clistr = int(clistr) for vm in self._connobjs[uri].list_vms(): if clistr == vm.get_id(): return vm elif clistr == vm.get_name(): return vm elif clistr == vm.get_uuid(): return vm def _cli_show_vm_helper(self, uri, clistr, page): src = self._get_manager() vm = self._find_vm_by_cli_str(uri, clistr) if not vm: src.err.show_err("%s does not have VM '%s'" % (uri, clistr), modal=True) return try: from .details import vmmDetails details = vmmDetails.get_instance(src, vm) if page == DETAILS_PERF: details.activate_performance_page() elif page == DETAILS_CONFIG: details.activate_config_page() elif page == DETAILS_CONSOLE: details.activate_console_page() elif page is None: details.activate_default_page() details.show() except Exception as e: src.err.show_err(_("Error launching details: %s") % str(e)) def _get_manager(self): from .manager import vmmManager return vmmManager.get_instance(None) @_show_startup_error def _launch_cli_window(self, uri, show_window, clistr): logging.debug("Launching requested window '%s'", show_window) if show_window == self.CLI_SHOW_MANAGER: manager = self._get_manager() manager.set_initial_selection(uri) manager.show() elif show_window == self.CLI_SHOW_DOMAIN_CREATOR: from .create import vmmCreate # Launch the manager here since there's no way to get # back to it. vmmCreate.show_instance(self._get_manager(), uri) elif show_window == self.CLI_SHOW_DOMAIN_EDITOR: self._cli_show_vm_helper(uri, clistr, DETAILS_CONFIG) elif show_window == self.CLI_SHOW_DOMAIN_PERFORMANCE: self._cli_show_vm_helper(uri, clistr, DETAILS_PERF) elif show_window == self.CLI_SHOW_DOMAIN_CONSOLE: self._cli_show_vm_helper(uri, clistr, DETAILS_CONSOLE) elif show_window == self.CLI_SHOW_HOST_SUMMARY: from .host import vmmHost vmmHost.show_instance(None, self._connobjs[uri]) else: raise RuntimeError("Unknown cli window command '%s'" % show_window) def _handle_conn_error(self, _conn, ConnectError): msg, details, title = ConnectError modal = self._can_exit() self.err.show_err(msg, details, title, modal=modal) self._exit_app_if_no_windows() @_show_startup_error def _handle_cli_command(self, actionobj, variant): ignore = actionobj uri = variant[0] show_window = variant[1] or self.CLI_SHOW_MANAGER domain = variant[2] logging.debug("processing cli command uri=%s show_window=%s domain=%s", uri, show_window, domain) if not uri: logging.debug("No cli action requested, launching default window") self._get_manager().show() return conn_is_new = uri not in self._connobjs conn = vmmConnectionManager.get_instance().add_conn(uri) if conn.is_active(): self.idle_add(self._launch_cli_window, uri, show_window, domain) return def _open_completed(_c, ConnectError): if ConnectError: if conn_is_new: logging.debug("Removing failed uri=%s", uri) vmmConnectionManager.get_instance().remove_conn(uri) self._handle_conn_error(conn, ConnectError) else: self._launch_cli_window(uri, show_window, domain) conn.connect_once("open-completed", _open_completed) conn.open()