mirror of
https://github.com/virt-manager/virt-manager.git
synced 2024-12-22 13:34:07 +03:00
6264167cc1
Currently, View->Consoles disables the menu items for serial devices when the VM is offline. This changes that behavior. This is useful, since it allows you to pre-select serial console before starting the VM, which can help ensure you don't miss any serial boot output. It also makes the UI interaction more intuitive. Signed-off-by: Cole Robinson <crobinso@redhat.com>
385 lines
12 KiB
Python
385 lines
12 KiB
Python
# Copyright (C) 2006, 2013 Red Hat, Inc.
|
|
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
|
|
#
|
|
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
# pylint: disable=wrong-import-order,ungrouped-imports
|
|
import gi
|
|
from gi.repository import Gdk
|
|
from gi.repository import Gtk
|
|
|
|
from virtinst import log
|
|
|
|
# We can use either 2.91 or 2.90. This is just to silence runtime warnings
|
|
try:
|
|
gi.require_version("Vte", "2.91")
|
|
log.debug("Using VTE API 2.91")
|
|
except ValueError: # pragma: no cover
|
|
gi.require_version("Vte", "2.90")
|
|
log.debug("Using VTE API 2.90")
|
|
from gi.repository import Vte
|
|
|
|
import libvirt
|
|
|
|
from ..baseclass import vmmGObject
|
|
|
|
|
|
class _DataStream(vmmGObject):
|
|
"""
|
|
Wrapper class for interacting with libvirt console stream
|
|
"""
|
|
def __init__(self, vm):
|
|
vmmGObject.__init__(self)
|
|
|
|
self.vm = vm
|
|
self.conn = vm.conn
|
|
|
|
self._stream = None
|
|
|
|
self._streamToTerminal = b""
|
|
self._terminalToStream = ""
|
|
|
|
def _cleanup(self):
|
|
self.close()
|
|
|
|
self.vm = None
|
|
self.conn = None
|
|
|
|
|
|
#################
|
|
# Internal APIs #
|
|
#################
|
|
|
|
def _display_data(self, terminal):
|
|
if not self._streamToTerminal:
|
|
return # pragma: no cover
|
|
|
|
terminal.feed(self._streamToTerminal)
|
|
self._streamToTerminal = b""
|
|
|
|
def _event_on_stream(self, stream, events, opaque):
|
|
ignore = stream
|
|
terminal = opaque
|
|
|
|
if (events & libvirt.VIR_EVENT_HANDLE_ERROR or
|
|
events & libvirt.VIR_EVENT_HANDLE_HANGUP): # pragma: no cover
|
|
log.debug("Received stream ERROR/HANGUP, closing console")
|
|
self.close()
|
|
return
|
|
|
|
if events & libvirt.VIR_EVENT_HANDLE_READABLE:
|
|
try:
|
|
got = self._stream.recv(1024 * 100)
|
|
except Exception: # pragma: no cover
|
|
log.exception("Error receiving stream data")
|
|
self.close()
|
|
return
|
|
|
|
if got == -2: # pragma: no cover
|
|
# This is basically EAGAIN
|
|
return
|
|
if len(got) == 0:
|
|
log.debug("Received EOF from stream, closing")
|
|
self.close()
|
|
return
|
|
|
|
queued_text = bool(self._streamToTerminal)
|
|
self._streamToTerminal += got
|
|
if not queued_text:
|
|
self.idle_add(self._display_data, terminal)
|
|
|
|
if (events & libvirt.VIR_EVENT_HANDLE_WRITABLE and
|
|
self._terminalToStream):
|
|
|
|
try:
|
|
done = self._stream.send(self._terminalToStream.encode())
|
|
except Exception: # pragma: no cover
|
|
log.exception("Error sending stream data")
|
|
self.close()
|
|
return
|
|
|
|
if done == -2: # pragma: no cover
|
|
# This is basically EAGAIN
|
|
return
|
|
|
|
self._terminalToStream = self._terminalToStream[done:]
|
|
|
|
if not self._terminalToStream:
|
|
self._stream.eventUpdateCallback(libvirt.VIR_STREAM_EVENT_READABLE |
|
|
libvirt.VIR_STREAM_EVENT_ERROR |
|
|
libvirt.VIR_STREAM_EVENT_HANGUP)
|
|
|
|
|
|
##############
|
|
# Public API #
|
|
##############
|
|
|
|
def open(self, dev, terminal):
|
|
if self._stream:
|
|
return
|
|
|
|
name = dev and dev.alias.name or None
|
|
log.debug("Opening console stream for dev=%s alias=%s",
|
|
dev, name)
|
|
# libxl doesn't set aliases, their open_console just defaults to
|
|
# opening the first console device, so don't force prescence of
|
|
# an alias
|
|
|
|
stream = self.conn.get_backend().newStream(libvirt.VIR_STREAM_NONBLOCK)
|
|
self.vm.open_console(name, stream)
|
|
self._stream = stream
|
|
|
|
self._stream.eventAddCallback((libvirt.VIR_STREAM_EVENT_READABLE |
|
|
libvirt.VIR_STREAM_EVENT_ERROR |
|
|
libvirt.VIR_STREAM_EVENT_HANGUP),
|
|
self._event_on_stream,
|
|
terminal)
|
|
|
|
def close(self):
|
|
if self._stream:
|
|
try:
|
|
self._stream.eventRemoveCallback()
|
|
except Exception: # pragma: no cover
|
|
log.exception("Error removing stream callback")
|
|
try:
|
|
self._stream.finish()
|
|
except Exception: # pragma: no cover
|
|
log.exception("Error finishing stream")
|
|
|
|
self._stream = None
|
|
|
|
def send_data(self, src, text, length, terminal):
|
|
"""
|
|
Callback when data has been entered into VTE terminal
|
|
"""
|
|
ignore = src
|
|
ignore = length
|
|
ignore = terminal
|
|
|
|
if self._stream is None:
|
|
return # pragma: no cover
|
|
|
|
self._terminalToStream += text
|
|
if self._terminalToStream:
|
|
self._stream.eventUpdateCallback(libvirt.VIR_STREAM_EVENT_READABLE |
|
|
libvirt.VIR_STREAM_EVENT_WRITABLE |
|
|
libvirt.VIR_STREAM_EVENT_ERROR |
|
|
libvirt.VIR_STREAM_EVENT_HANGUP)
|
|
|
|
|
|
class vmmSerialConsole(vmmGObject):
|
|
@staticmethod
|
|
def can_connect(_vm, dev):
|
|
"""
|
|
Check if we think we can actually open passed console/serial dev
|
|
"""
|
|
usable_types = ["pty"]
|
|
ctype = dev.type
|
|
|
|
err = ""
|
|
|
|
if ctype not in usable_types:
|
|
err = (_("Console for device type '%s' is not supported") % ctype)
|
|
|
|
return err
|
|
|
|
@staticmethod
|
|
def get_serialcon_devices(vm):
|
|
serials = vm.xmlobj.devices.serial
|
|
consoles = vm.xmlobj.devices.console
|
|
if serials and vm.serial_is_console_dup(serials[0]):
|
|
consoles.pop(0)
|
|
return serials + consoles
|
|
|
|
|
|
def __init__(self, vm, target_port, name):
|
|
vmmGObject.__init__(self)
|
|
|
|
self.vm = vm
|
|
self.target_port = target_port
|
|
self.name = name
|
|
self.lastpath = None
|
|
|
|
self._datastream = _DataStream(self.vm)
|
|
|
|
self._serial_popup = None
|
|
self._serial_copy = None
|
|
self._serial_paste = None
|
|
self._init_popup()
|
|
|
|
self._vteterminal = None
|
|
self._init_terminal()
|
|
|
|
self._box = None
|
|
self._error_label = None
|
|
self._init_ui()
|
|
|
|
self.vm.connect("state-changed", self._vm_status_changed)
|
|
|
|
def _cleanup(self):
|
|
self._datastream.cleanup()
|
|
self._datastream = None
|
|
|
|
self.vm = None
|
|
self._vteterminal = None
|
|
self._box = None
|
|
|
|
|
|
###########
|
|
# UI init #
|
|
###########
|
|
|
|
def _init_terminal(self):
|
|
self._vteterminal = Vte.Terminal()
|
|
self._vteterminal.set_scrollback_lines(1000)
|
|
self._vteterminal.set_audible_bell(False)
|
|
self._vteterminal.get_accessible().set_name("Serial Terminal")
|
|
|
|
self._vteterminal.connect("button-press-event",
|
|
self._show_serial_rcpopup)
|
|
self._vteterminal.connect("commit",
|
|
self._datastream.send_data, self._vteterminal)
|
|
self._vteterminal.show()
|
|
|
|
def _init_popup(self):
|
|
self._serial_popup = Gtk.Menu()
|
|
self._serial_popup.get_accessible().set_name("serial-popup-menu")
|
|
|
|
self._serial_copy = Gtk.MenuItem.new_with_mnemonic(_("_Copy"))
|
|
self._serial_copy.connect("activate", self._serial_copy_text)
|
|
self._serial_popup.add(self._serial_copy)
|
|
|
|
self._serial_paste = Gtk.MenuItem.new_with_mnemonic(_("_Paste"))
|
|
self._serial_paste.connect("activate", self._serial_paste_text)
|
|
self._serial_popup.add(self._serial_paste)
|
|
|
|
def _init_ui(self):
|
|
self._box = Gtk.Notebook()
|
|
self._box.set_show_tabs(False)
|
|
self._box.set_show_border(False)
|
|
|
|
align = Gtk.Box()
|
|
align.set_border_width(2)
|
|
evbox = Gtk.EventBox()
|
|
evbox.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(0, 0, 0))
|
|
terminalbox = Gtk.HBox()
|
|
scrollbar = Gtk.VScrollbar()
|
|
self._error_label = Gtk.Label()
|
|
self._error_label.set_width_chars(40)
|
|
self._error_label.set_line_wrap(True)
|
|
|
|
if self._vteterminal:
|
|
scrollbar.set_adjustment(self._vteterminal.get_vadjustment())
|
|
align.pack_start(self._vteterminal, True, True, 0)
|
|
|
|
evbox.add(align)
|
|
terminalbox.pack_start(evbox, True, True, 0)
|
|
terminalbox.pack_start(scrollbar, False, False, 0)
|
|
|
|
self._box.append_page(terminalbox, Gtk.Label(""))
|
|
self._box.append_page(self._error_label, Gtk.Label(""))
|
|
self._box.show_all()
|
|
|
|
scrollbar.hide()
|
|
scrollbar.get_adjustment().connect(
|
|
"changed", self._scrollbar_adjustment_changed, scrollbar)
|
|
|
|
|
|
###################
|
|
# Private methods #
|
|
###################
|
|
|
|
def _show_error(self, msg):
|
|
self._error_label.set_markup("<b>%s</b>" % msg)
|
|
self._box.set_current_page(1)
|
|
|
|
def _lookup_dev(self):
|
|
devs = vmmSerialConsole.get_serialcon_devices(self.vm)
|
|
found = None
|
|
for dev in devs:
|
|
port = dev.get_xml_idx()
|
|
path = dev.source.path
|
|
|
|
if port == self.target_port:
|
|
if path != self.lastpath:
|
|
log.debug("Serial console '%s' path changed to %s",
|
|
self.target_port, path)
|
|
self.lastpath = path
|
|
found = dev
|
|
break
|
|
|
|
if not found: # pragma: no cover
|
|
log.debug("No devices found for serial target port '%s'",
|
|
self.target_port)
|
|
self.lastpath = None
|
|
return found
|
|
|
|
|
|
##############
|
|
# Public API #
|
|
##############
|
|
|
|
def close(self):
|
|
if self._datastream:
|
|
self._datastream.close()
|
|
|
|
def get_box(self):
|
|
return self._box
|
|
|
|
def has_focus(self):
|
|
return bool(self._vteterminal and
|
|
self._vteterminal.get_property("has-focus"))
|
|
|
|
def set_focus_callbacks(self, in_cb, out_cb):
|
|
self._vteterminal.connect("focus-in-event", in_cb)
|
|
self._vteterminal.connect("focus-out-event", out_cb)
|
|
|
|
def open_console(self):
|
|
try:
|
|
dev = self._lookup_dev()
|
|
self._datastream.open(dev, self._vteterminal)
|
|
self._box.set_current_page(0)
|
|
return True
|
|
except Exception as e:
|
|
log.exception("Error opening serial console")
|
|
self._show_error(_("Error connecting to text console: %s") % e)
|
|
try:
|
|
self._datastream.close()
|
|
except Exception: # pragma: no cover
|
|
pass
|
|
return False
|
|
|
|
|
|
################
|
|
# UI listeners #
|
|
################
|
|
|
|
def _vm_status_changed(self, vm):
|
|
if vm.status() in [libvirt.VIR_DOMAIN_RUNNING]:
|
|
self.open_console()
|
|
else:
|
|
self._datastream.close()
|
|
|
|
def _scrollbar_adjustment_changed(self, adjustment, scrollbar):
|
|
scrollbar.set_visible(
|
|
adjustment.get_upper() > adjustment.get_page_size())
|
|
|
|
def _show_serial_rcpopup(self, src, event):
|
|
if event.button != 3:
|
|
return
|
|
|
|
self._serial_popup.show_all()
|
|
|
|
if src.get_has_selection():
|
|
self._serial_copy.set_sensitive(True)
|
|
else:
|
|
self._serial_copy.set_sensitive(False)
|
|
self._serial_popup.popup_at_pointer(event)
|
|
|
|
def _serial_copy_text(self, src_ignore):
|
|
self._vteterminal.copy_clipboard()
|
|
|
|
def _serial_paste_text(self, src_ignore):
|
|
self._vteterminal.paste_clipboard()
|