1# Copyright (C) 2006-2008, 2015 Red Hat, Inc. 2# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com> 3# Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@redhat.com> 4# 5# This work is licensed under the GNU GPLv2 or later. 6# See the COPYING file in the top-level directory. 7 8from gi.repository import Gtk 9from gi.repository import Gdk 10 11from virtinst import log 12 13from .serialcon import vmmSerialConsole 14from .sshtunnels import ConnectionInfo 15from .viewers import SpiceViewer, VNCViewer, have_spice_gtk 16from ..baseclass import vmmGObject, vmmGObjectUI 17from ..lib.keyring import vmmKeyring 18 19 20# console-pages IDs 21(_CONSOLE_PAGE_UNAVAILABLE, 22 _CONSOLE_PAGE_SERIAL, 23 _CONSOLE_PAGE_GRAPHICS) = range(3) 24 25# console-gfx-pages IDs 26(_GFX_PAGE_VIEWER, 27 _GFX_PAGE_AUTH, 28 _GFX_PAGE_UNAVAILABLE, 29 _GFX_PAGE_CONNECT) = range(4) 30 31 32class _TimedRevealer(vmmGObject): 33 """ 34 Revealer for the fullscreen toolbar, with a bit of extra logic to 35 hide/show based on mouse over 36 """ 37 def __init__(self, toolbar): 38 vmmGObject.__init__(self) 39 40 self._in_fullscreen = False 41 self._timeout_id = None 42 43 self._revealer = Gtk.Revealer() 44 self._revealer.add(toolbar) 45 46 # Adding the revealer to the eventbox seems to ensure the 47 # eventbox always has 1 invisible pixel showing at the top of the 48 # screen, which we can use to grab the pointer event to show 49 # the hidden toolbar. 50 51 self._ebox = Gtk.EventBox() 52 self._ebox.add(self._revealer) 53 self._ebox.set_halign(Gtk.Align.CENTER) 54 self._ebox.set_valign(Gtk.Align.START) 55 self._ebox.show_all() 56 57 self._ebox.connect("enter-notify-event", self._enter_notify) 58 self._ebox.connect("leave-notify-event", self._enter_notify) 59 60 def _cleanup(self): 61 self._ebox.destroy() 62 self._ebox = None 63 self._revealer.destroy() 64 self._revealer = None 65 self._timeout_id = None 66 67 def _enter_notify(self, ignore1, ignore2): 68 x, y = self._ebox.get_pointer() 69 alloc = self._ebox.get_allocation() 70 entered = bool(x >= 0 and y >= 0 and 71 x < alloc.width and y < alloc.height) 72 73 if not self._in_fullscreen: 74 return 75 76 # Pointer exited the toolbar, and toolbar is revealed. Schedule 77 # a timeout to close it, if one isn't already scheduled 78 if not entered and self._revealer.get_reveal_child(): 79 self._schedule_unreveal_timeout(1000) 80 return 81 82 self._unregister_timeout() 83 if entered and not self._revealer.get_reveal_child(): 84 self._revealer.set_reveal_child(True) 85 86 def _schedule_unreveal_timeout(self, timeout): 87 if self._timeout_id: 88 return # pragma: no cover 89 90 def cb(): 91 self._revealer.set_reveal_child(False) 92 self._timeout_id = None 93 self._timeout_id = self.timeout_add(timeout, cb) 94 95 def _unregister_timeout(self): 96 if self._timeout_id: # pragma: no cover 97 self.remove_gobject_timeout(self._timeout_id) 98 self._timeout_id = None 99 100 def force_reveal(self, val): 101 self._unregister_timeout() 102 self._in_fullscreen = val 103 self._revealer.set_reveal_child(val) 104 self._schedule_unreveal_timeout(2000) 105 106 def get_overlay_widget(self): 107 return self._ebox 108 109 110def build_keycombo_menu(on_send_key_fn): 111 menu = Gtk.Menu() 112 113 def make_item(accel, combo): 114 name = Gtk.accelerator_get_label(*Gtk.accelerator_parse(accel)) 115 item = Gtk.MenuItem(name) 116 item.connect("activate", on_send_key_fn, combo) 117 118 menu.add(item) 119 120 make_item("<Control><Alt>BackSpace", ["Control_L", "Alt_L", "BackSpace"]) 121 make_item("<Control><Alt>Delete", ["Control_L", "Alt_L", "Delete"]) 122 menu.add(Gtk.SeparatorMenuItem()) 123 124 for i in range(1, 13): 125 make_item("<Control><Alt>F%d" % i, ["Control_L", "Alt_L", "F%d" % i]) 126 menu.add(Gtk.SeparatorMenuItem()) 127 128 make_item("Print", ["Print"]) 129 130 menu.show_all() 131 return menu 132 133 134class vmmOverlayToolbar: 135 def __init__(self, on_leave_fn, on_send_key_fn): 136 self._send_key_button = None 137 self._keycombo_menu = None 138 self._toolbar = None 139 140 self.timed_revealer = None 141 self._init_ui(on_leave_fn, on_send_key_fn) 142 143 def _init_ui(self, on_leave_fn, on_send_key_fn): 144 self._keycombo_menu = build_keycombo_menu(on_send_key_fn) 145 146 self._toolbar = Gtk.Toolbar() 147 self._toolbar.set_show_arrow(False) 148 self._toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ) 149 self._toolbar.get_accessible().set_name("Fullscreen Toolbar") 150 151 # Exit button 152 button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_LEAVE_FULLSCREEN) 153 button.set_tooltip_text(_("Leave fullscreen")) 154 button.show() 155 button.get_accessible().set_name("Fullscreen Exit") 156 self._toolbar.add(button) 157 button.connect("clicked", on_leave_fn) 158 159 self._send_key_button = Gtk.ToolButton() 160 self._send_key_button.set_icon_name( 161 "preferences-desktop-keyboard-shortcuts") 162 self._send_key_button.set_tooltip_text(_("Send key combination")) 163 self._send_key_button.show_all() 164 self._send_key_button.connect("clicked", 165 self._on_send_key_button_clicked_cb) 166 self._send_key_button.get_accessible().set_name("Fullscreen Send Key") 167 self._toolbar.add(self._send_key_button) 168 169 self.timed_revealer = _TimedRevealer(self._toolbar) 170 171 def _on_send_key_button_clicked_cb(self, src): 172 event = Gtk.get_current_event() 173 win = self._toolbar.get_window() 174 rect = Gdk.Rectangle() 175 176 rect.y = win.get_height() 177 self._keycombo_menu.popup_at_rect(win, rect, 178 Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event) 179 180 def cleanup(self): 181 self._keycombo_menu.destroy() 182 self._keycombo_menu = None 183 self._toolbar.destroy() 184 self._toolbar = None 185 self.timed_revealer.cleanup() 186 self.timed_revealer = None 187 188 189class _ConsoleMenu: 190 """ 191 Helper class for building the text/graphical console menu list 192 """ 193 194 ################ 195 # Internal API # 196 ################ 197 198 def _build_serial_menu_items(self, vm): 199 devs = vmmSerialConsole.get_serialcon_devices(vm) 200 if len(devs) == 0: 201 return [[_("No text console available"), None, None]] 202 203 ret = [] 204 for dev in devs: 205 if dev.DEVICE_TYPE == "console": 206 label = _("Text Console %d") % (dev.get_xml_idx() + 1) 207 else: 208 label = _("Serial %d") % (dev.get_xml_idx() + 1) 209 210 tooltip = vmmSerialConsole.can_connect(vm, dev) 211 ret.append([label, dev, tooltip]) 212 return ret 213 214 def _build_graphical_menu_items(self, vm): 215 devs = vm.xmlobj.devices.graphics 216 if len(devs) == 0: 217 return [[_("No graphical console available"), None, None]] 218 219 from ..device.gfxdetails import vmmGraphicsDetails 220 221 ret = [] 222 for idx, dev in enumerate(devs): 223 label = (_("Graphical Console") + " " + 224 vmmGraphicsDetails.graphics_pretty_type_simple(dev.type)) 225 226 tooltip = None 227 if idx > 0: 228 label += " %s" % (idx + 1) 229 tooltip = _("virt-manager does not support more " 230 "than one graphical console") 231 232 ret.append([label, dev, tooltip]) 233 return ret 234 235 236 ############## 237 # Public API # 238 ############## 239 240 def rebuild_menu(self, vm, submenu, toggled_cb): 241 oldlabel = None 242 for child in submenu.get_children(): 243 if hasattr(child, 'get_active') and child.get_active(): 244 oldlabel = child.get_label() 245 submenu.remove(child) 246 247 graphics = self._build_graphical_menu_items(vm) 248 serials = self._build_serial_menu_items(vm) 249 250 # Use label == None to tell the loop to add a separator 251 items = graphics + [[None, None, None]] + serials 252 253 last_item = None 254 for (label, dev, tooltip) in items: 255 if label is None: 256 submenu.add(Gtk.SeparatorMenuItem()) 257 continue 258 259 cb = toggled_cb 260 cbdata = dev 261 sensitive = dev and not tooltip 262 263 active = False 264 if oldlabel is None and sensitive: 265 # Select the first selectable option 266 oldlabel = label 267 if label == oldlabel: 268 active = True 269 270 item = Gtk.RadioMenuItem() 271 if last_item is None: 272 last_item = item 273 else: 274 item.join_group(last_item) 275 276 item.set_label(label) 277 item.set_active(active and sensitive) 278 if cbdata and sensitive: 279 item.connect("toggled", cb, cbdata) 280 281 item.set_sensitive(sensitive) 282 item.set_tooltip_text(tooltip or None) 283 submenu.add(item) 284 285 submenu.show_all() 286 287 def activate_default(self, menu): 288 for child in menu.get_children(): 289 if child.get_sensitive() and hasattr(child, "toggled"): 290 child.toggled() 291 return True 292 return False 293 294 295class vmmConsolePages(vmmGObjectUI): 296 """ 297 Handles all the complex UI handling dictated by the spice/vnc widgets 298 """ 299 __gsignals__ = { 300 "page-changed": (vmmGObjectUI.RUN_FIRST, None, []), 301 "leave-fullscreen": (vmmGObjectUI.RUN_FIRST, None, []), 302 "change-title": (vmmGObjectUI.RUN_FIRST, None, []), 303 } 304 305 def __init__(self, vm, builder, topwin): 306 vmmGObjectUI.__init__(self, "console.ui", 307 None, builder=builder, topwin=topwin) 308 309 self.vm = vm 310 self.top_box = self.widget("console-pages") 311 self._pointer_is_grabbed = False 312 313 # State for disabling modifiers when keyboard is grabbed 314 self._accel_groups = Gtk.accel_groups_from_object(self.topwin) 315 self._gtk_settings_accel = None 316 self._gtk_settings_mnemonic = None 317 318 # Initialize display widget 319 self._viewer = None 320 self._viewer_connect_clicked = False 321 self._in_fullscreen = False 322 323 # Fullscreen toolbar 324 self._keycombo_menu = build_keycombo_menu(self._do_send_key) 325 self._console_list_menu = Gtk.Menu() 326 self._console_list_menu.connect("show", 327 self._populate_console_list_menu) 328 329 self._overlay_toolbar_fullscreen = vmmOverlayToolbar( 330 on_leave_fn=self._leave_fullscreen, 331 on_send_key_fn=self._do_send_key) 332 self.widget("console-overlay").add_overlay( 333 self._overlay_toolbar_fullscreen.timed_revealer.get_overlay_widget()) 334 335 # Make viewer widget background always be black 336 black = Gdk.Color(0, 0, 0) 337 self.widget("console-gfx-viewport").modify_bg(Gtk.StateType.NORMAL, 338 black) 339 340 self.widget("console-pages").set_show_tabs(False) 341 self.widget("serial-pages").set_show_tabs(False) 342 self.widget("console-gfx-pages").set_show_tabs(False) 343 344 self._consolemenu = _ConsoleMenu() 345 self._serial_consoles = [] 346 347 # Signals are added by vmmVMWindow. Don't use connect_signals here 348 # or it changes will be overwritten 349 350 self.builder.connect_signals({ 351 "on_console_pages_switch_page": self._page_changed_cb, 352 "on_console_auth_password_activate": self._auth_login_cb, 353 "on_console_auth_login_clicked": self._auth_login_cb, 354 "on_console_connect_button_clicked": self._connect_button_clicked_cb, 355 }) 356 357 self.widget("console-gfx-scroll").connect("size-allocate", 358 self._scroll_size_allocate) 359 self.widget("console-gfx-pages").connect("switch-page", 360 self._page_changed_cb) 361 362 363 def _cleanup(self): 364 self.vm = None 365 366 if self._viewer: 367 self._viewer.cleanup() # pragma: no cover 368 self._viewer = None 369 370 self._overlay_toolbar_fullscreen.cleanup() 371 372 for serial in self._serial_consoles: 373 serial.cleanup() 374 self._serial_consoles = [] 375 376 377 ################# 378 # Internal APIs # 379 ################# 380 381 def _disable_modifiers(self): 382 if self._gtk_settings_accel is not None: 383 return # pragma: no cover 384 385 for g in self._accel_groups: 386 self.topwin.remove_accel_group(g) 387 388 settings = Gtk.Settings.get_default() 389 self._gtk_settings_accel = settings.get_property('gtk-menu-bar-accel') 390 settings.set_property('gtk-menu-bar-accel', None) 391 392 self._gtk_settings_mnemonic = settings.get_property( 393 "gtk-enable-mnemonics") 394 settings.set_property("gtk-enable-mnemonics", False) 395 396 def _enable_modifiers(self): 397 if self._gtk_settings_accel is None: 398 return 399 400 settings = Gtk.Settings.get_default() 401 settings.set_property('gtk-menu-bar-accel', self._gtk_settings_accel) 402 self._gtk_settings_accel = None 403 404 if self._gtk_settings_mnemonic is not None: 405 settings.set_property("gtk-enable-mnemonics", 406 self._gtk_settings_mnemonic) 407 408 for g in self._accel_groups: 409 self.topwin.add_accel_group(g) 410 411 def _do_send_key(self, src, keys): 412 ignore = src 413 414 if keys is not None: 415 self._viewer.console_send_keys(keys) 416 417 418 ########################### 419 # Resize and scaling APIs # 420 ########################### 421 422 def _scroll_size_allocate(self, src_ignore, req): 423 if not self._viewer: 424 return 425 426 res = self._viewer.console_get_desktop_resolution() 427 if res is None: 428 if not self.config.CLITestOptions.fake_console_resolution: 429 return 430 res = (800, 600) 431 432 scroll = self.widget("console-gfx-scroll") 433 is_scale = self._viewer.console_get_scaling() 434 is_resizeguest = self._viewer.console_get_resizeguest() 435 436 dx = 0 437 dy = 0 438 align_ratio = float(req.width) / float(req.height) 439 440 # pylint: disable=unpacking-non-sequence 441 desktop_w, desktop_h = res 442 desktop_ratio = float(desktop_w) / float(desktop_h) 443 444 if is_scale: 445 # Make sure we never show scrollbars when scaling 446 scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 447 else: 448 scroll.set_policy(Gtk.PolicyType.AUTOMATIC, 449 Gtk.PolicyType.AUTOMATIC) 450 451 if is_resizeguest: 452 # With resize guest, we don't want to maintain aspect ratio, 453 # since the guest can resize to arbitrary resolutions. 454 viewer_alloc = Gdk.Rectangle() 455 viewer_alloc.width = req.width 456 viewer_alloc.height = req.height 457 self._viewer.console_size_allocate(viewer_alloc) 458 return 459 460 if not is_scale: 461 # Scaling disabled is easy, just force the VNC widget size. Since 462 # we are inside a scrollwindow, it shouldn't cause issues. 463 self._viewer.console_set_size_request(desktop_w, desktop_h) 464 return 465 466 # Make sure there is no hard size requirement so we can scale down 467 self._viewer.console_set_size_request(-1, -1) 468 469 # Make sure desktop aspect ratio is maintained 470 if align_ratio > desktop_ratio: 471 desktop_w = int(req.height * desktop_ratio) 472 desktop_h = req.height 473 dx = (req.width - desktop_w) // 2 474 475 else: 476 desktop_w = req.width 477 desktop_h = int(req.width // desktop_ratio) 478 dy = (req.height - desktop_h) // 2 479 480 viewer_alloc = Gdk.Rectangle() 481 viewer_alloc.x = dx 482 viewer_alloc.y = dy 483 viewer_alloc.width = desktop_w 484 viewer_alloc.height = desktop_h 485 self._viewer.console_size_allocate(viewer_alloc) 486 487 def _viewer_get_resizeguest_tooltip(self): 488 tooltip = "" 489 if self._viewer: 490 if self._viewer.viewer_type != "spice": 491 tooltip = ( 492 _("Graphics type '%s' does not support auto resize.") % 493 self._viewer.viewer_type) 494 elif not self._viewer.console_has_agent(): 495 tooltip = _("Guest agent is not available.") 496 return tooltip 497 498 def _sync_resizeguest_with_display(self): 499 if not self._viewer: 500 return 501 502 val = bool(self.vm.get_console_resizeguest()) 503 self._viewer.console_set_resizeguest(val) 504 self.widget("console-gfx-scroll").queue_resize() 505 506 def _set_size_to_vm(self): 507 # Resize the console to best fit the VM resolution 508 if not self._viewer: 509 return # pragma: no cover 510 if not self._viewer.console_get_desktop_resolution(): 511 return # pragma: no cover 512 513 top_w, top_h = self.topwin.get_size() 514 viewer_alloc = self.widget("console-gfx-scroll").get_allocation() 515 desktop_w, desktop_h = self._viewer.console_get_desktop_resolution() 516 517 self.topwin.unmaximize() 518 self.topwin.resize( 519 desktop_w + (top_w - viewer_alloc.width), 520 desktop_h + (top_h - viewer_alloc.height)) 521 522 523 ################ 524 # Scaling APIs # 525 ################ 526 527 def _sync_scaling_with_display(self): 528 if not self._viewer: 529 return 530 531 fs = self._in_fullscreen 532 curscale = self._viewer.console_get_scaling() 533 scale_type = self.vm.get_console_scaling() 534 535 if (scale_type == self.config.CONSOLE_SCALE_NEVER and 536 curscale is True): 537 self._viewer.console_set_scaling(False) 538 elif (scale_type == self.config.CONSOLE_SCALE_ALWAYS and 539 curscale is False): 540 self._viewer.console_set_scaling(True) 541 elif (scale_type == self.config.CONSOLE_SCALE_FULLSCREEN and 542 curscale != fs): 543 self._viewer.console_set_scaling(fs) 544 545 # Refresh viewer size 546 self.widget("console-gfx-scroll").queue_resize() 547 548 549 ################### 550 # Fullscreen APIs # 551 ################### 552 553 def _leave_fullscreen(self, ignore=None): 554 self.emit("leave-fullscreen") 555 556 def _change_fullscreen(self, do_fullscreen): 557 if do_fullscreen: 558 self._in_fullscreen = True 559 self.topwin.fullscreen() 560 self._overlay_toolbar_fullscreen.timed_revealer.force_reveal(True) 561 else: 562 self._in_fullscreen = False 563 self._overlay_toolbar_fullscreen.timed_revealer.force_reveal(False) 564 self.topwin.unfullscreen() 565 566 self._sync_scaling_with_display() 567 568 569 ########################## 570 # State tracking methods # 571 ########################## 572 573 def _show_vm_status_unavailable(self): 574 if self.vm.is_crashed(): # pragma: no cover 575 self._activate_vm_unavailable_page(_("Guest has crashed.")) 576 else: 577 self._activate_vm_unavailable_page(_("Guest is not running.")) 578 579 def _close_viewer(self): 580 self._leave_fullscreen() 581 self._viewer_connect_clicked = False 582 583 for serial in self._serial_consoles: 584 serial.close() 585 586 if self._viewer is None: 587 return 588 self._viewer.console_remove_display_from_widget( 589 self.widget("console-gfx-viewport")) 590 self._viewer.cleanup() 591 self._viewer = None 592 log.debug("Viewer disconnected") 593 594 def _refresh_vm_state(self): 595 self._activate_default_console_page() 596 597 598 ########################### 599 # console page navigation # 600 ########################### 601 602 def _activate_gfx_unavailable_page(self, msg): 603 self._close_viewer() 604 self.widget("console-gfx-pages").set_current_page( 605 _GFX_PAGE_UNAVAILABLE) 606 if msg: 607 self.widget("console-gfx-unavailable").set_label( 608 "<b>" + msg + "</b>") 609 610 def _activate_vm_unavailable_page(self, msg): 611 """ 612 This is the top level error page. We should only set it for very 613 specific error cases, because when it is set and the VM is running 614 we take that to mean we should attempt to connect to the default 615 console. 616 """ 617 self._close_viewer() 618 self.widget("console-pages").set_current_page( 619 _CONSOLE_PAGE_UNAVAILABLE) 620 if msg: 621 self.widget("console-unavailable").set_label( 622 "<b>" + msg + "</b>") 623 self._activate_gfx_unavailable_page(msg) 624 625 def _activate_auth_page(self, withPassword, withUsername): 626 (pw, username) = vmmKeyring.get_instance().get_console_password(self.vm) 627 628 self.widget("console-auth-password").set_visible(withPassword) 629 self.widget("label-auth-password").set_visible(withPassword) 630 631 self.widget("console-auth-username").set_visible(withUsername) 632 self.widget("label-auth-username").set_visible(withUsername) 633 634 self.widget("console-auth-username").set_text(username) 635 self.widget("console-auth-password").set_text(pw) 636 637 has_keyring = vmmKeyring.get_instance().is_available() 638 remember = bool(withPassword and pw) or (withUsername and username) 639 remember = has_keyring and remember 640 self.widget("console-auth-remember").set_sensitive(has_keyring) 641 self.widget("console-auth-remember").set_active(remember) 642 643 self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_AUTH) 644 645 if withUsername: 646 self.widget("console-auth-username").grab_focus() 647 else: 648 self.widget("console-auth-password").grab_focus() 649 650 def _activate_gfx_viewer_page(self): 651 self.widget("console-pages").set_current_page(_CONSOLE_PAGE_GRAPHICS) 652 self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_VIEWER) 653 if self._viewer: 654 self._viewer.console_grab_focus() 655 656 def _activate_gfx_connect_page(self): 657 self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_CONNECT) 658 659 def _viewer_is_visible(self): 660 is_visible = self.widget("console-pages").is_visible() 661 cpage = self.widget("console-pages").get_current_page() 662 gpage = self.widget("console-gfx-pages").get_current_page() 663 664 return bool( 665 is_visible and 666 cpage == _CONSOLE_PAGE_GRAPHICS and 667 gpage == _GFX_PAGE_VIEWER and 668 self._viewer and self._viewer.console_is_open()) 669 670 def _viewer_can_usb_redirect(self): 671 return (self._viewer_is_visible() and 672 self._viewer.console_has_usb_redirection() and 673 self.vm.has_spicevmc_type_redirdev()) 674 675 676 ######################### 677 # Viewer login attempts # 678 ######################### 679 680 def _init_viewer(self): 681 if self._viewer or not self.is_visible(): 682 # Don't try and login for these cases 683 return 684 685 ginfo = None 686 try: 687 gdevs = self.vm.xmlobj.devices.graphics 688 gdev = gdevs and gdevs[0] or None 689 if gdev: 690 ginfo = ConnectionInfo(self.vm.conn, gdev) 691 except Exception as e: # pragma: no cover 692 # We can fail here if VM is destroyed: xen is a bit racy 693 # and can't handle domain lookups that soon after 694 log.exception("Getting graphics console failed: %s", str(e)) 695 return 696 697 if ginfo is None: 698 log.debug("No graphics configured for guest") 699 self._activate_gfx_unavailable_page( 700 _("Graphical console not configured for guest")) 701 return 702 703 if ginfo.gtype not in self.config.embeddable_graphics(): 704 log.debug("Don't know how to show graphics type '%s' " 705 "disabling console page", ginfo.gtype) 706 707 msg = (_("Cannot display graphical console type '%s'") 708 % ginfo.gtype) 709 710 self._activate_gfx_unavailable_page(msg) 711 return 712 713 if (not self.vm.get_console_autoconnect() and 714 not self._viewer_connect_clicked): 715 self._activate_gfx_connect_page() 716 return 717 718 self._activate_gfx_unavailable_page( 719 _("Connecting to graphical console for guest")) 720 721 log.debug("Starting connect process for %s", ginfo.logstring()) 722 try: 723 if ginfo.gtype == "vnc": 724 viewer_class = VNCViewer 725 elif ginfo.gtype == "spice": 726 if not have_spice_gtk: # pragma: no cover 727 raise RuntimeError("Error opening Spice console, " 728 "SpiceClientGtk missing") 729 viewer_class = SpiceViewer 730 731 self._viewer = viewer_class(self.vm, ginfo) 732 self._connect_viewer_signals() 733 734 self._viewer.console_open() 735 except Exception as e: 736 log.exception("Error connecting to graphical console") 737 self._activate_gfx_unavailable_page( 738 _("Error connecting to graphical console:\n%s") % e) 739 740 def _set_credentials(self, src_ignore=None): 741 passwd = self.widget("console-auth-password") 742 username = self.widget("console-auth-username") 743 744 if passwd.get_visible(): 745 self._viewer.console_set_password(passwd.get_text()) 746 if username.get_visible(): 747 self._viewer.console_set_username(username.get_text()) 748 749 if self.widget("console-auth-remember").get_active(): 750 vmmKeyring.get_instance().set_console_password( 751 self.vm, passwd.get_text(), username.get_text()) 752 else: 753 vmmKeyring.get_instance().del_console_password(self.vm) 754 755 756 ########################## 757 # Viewer signal handling # 758 ########################## 759 760 def _viewer_add_display(self, ignore, display): 761 self.widget("console-gfx-viewport").add(display) 762 763 # Sync initial settings 764 self._sync_scaling_with_display() 765 self._sync_resizeguest_with_display() 766 767 def _pointer_grabbed(self, ignore): 768 self._pointer_is_grabbed = True 769 self.emit("change-title") 770 771 def _pointer_ungrabbed(self, ignore): 772 self._pointer_is_grabbed = False 773 self.emit("change-title") 774 775 def _viewer_allocate_cb(self, src, ignore): 776 self.widget("console-gfx-scroll").queue_resize() 777 778 def _viewer_keyboard_grab_cb(self, src): 779 self._viewer_sync_modifiers() 780 781 def _serial_focus_changed_cb(self, src, event): 782 self._viewer_sync_modifiers() 783 784 def _viewer_sync_modifiers(self): 785 serial_has_focus = any([s.has_focus() for s in self._serial_consoles]) 786 viewer_keyboard_grab = (self._viewer and 787 self._viewer.console_has_keyboard_grab()) 788 789 if serial_has_focus or viewer_keyboard_grab: 790 self._disable_modifiers() 791 else: 792 self._enable_modifiers() 793 794 def _viewer_auth_error(self, ignore, errmsg, viewer_will_disconnect): 795 errmsg = _("Viewer authentication error: %s") % errmsg 796 self.err.val_err(errmsg) 797 798 if viewer_will_disconnect: 799 # GtkVNC will disconnect after an auth error, so lets do it for 800 # them and re-init the viewer (which will be triggered by 801 # _refresh_vm_state if needed) 802 self._activate_vm_unavailable_page(errmsg) 803 804 self._refresh_vm_state() 805 806 def _viewer_need_auth(self, ignore, withPassword, withUsername): 807 self._activate_auth_page(withPassword, withUsername) 808 809 def _viewer_agent_connected(self, ignore): 810 # Tell the vmwindow to trigger a state refresh, since 811 # resizeguest setting depends on the agent value 812 if self.widget("console-pages").is_visible(): # pragma: no cover 813 self.emit("page-changed") 814 815 def _viewer_usb_redirect_error(self, ignore, errstr): 816 self.err.show_err( 817 _("USB redirection error"), 818 text2=str(errstr), modal=True) # pragma: no cover 819 820 def _viewer_disconnected_set_page(self, errdetails, ssherr): 821 if self.vm.is_runable(): # pragma: no cover 822 # Exit was probably for legitimate reasons 823 self._show_vm_status_unavailable() 824 return 825 826 msg = _("Viewer was disconnected.") 827 if errdetails: 828 msg += "\n" + errdetails 829 if ssherr: 830 log.debug("SSH tunnel error output: %s", ssherr) 831 msg += "\n\n" 832 msg += _("SSH tunnel error output: %s") % ssherr 833 834 self._activate_gfx_unavailable_page(msg) 835 836 def _viewer_disconnected(self, ignore, errdetails, ssherr): 837 self._activate_gfx_unavailable_page(_("Viewer disconnected.")) 838 log.debug("Viewer disconnected") 839 840 # Make sure modifiers are set correctly 841 self._viewer_sync_modifiers() 842 843 self._viewer_disconnected_set_page(errdetails, ssherr) 844 845 def _viewer_connected(self, ignore): 846 log.debug("Viewer connected") 847 self._activate_gfx_viewer_page() 848 849 # Make sure modifiers are set correctly 850 self._viewer_sync_modifiers() 851 852 def _connect_viewer_signals(self): 853 self._viewer.connect("add-display-widget", self._viewer_add_display) 854 self._viewer.connect("pointer-grab", self._pointer_grabbed) 855 self._viewer.connect("pointer-ungrab", self._pointer_ungrabbed) 856 self._viewer.connect("size-allocate", self._viewer_allocate_cb) 857 self._viewer.connect("keyboard-grab", self._viewer_keyboard_grab_cb) 858 self._viewer.connect("keyboard-ungrab", self._viewer_keyboard_grab_cb) 859 self._viewer.connect("connected", self._viewer_connected) 860 self._viewer.connect("disconnected", self._viewer_disconnected) 861 self._viewer.connect("auth-error", self._viewer_auth_error) 862 self._viewer.connect("need-auth", self._viewer_need_auth) 863 self._viewer.connect("agent-connected", self._viewer_agent_connected) 864 self._viewer.connect("usb-redirect-error", 865 self._viewer_usb_redirect_error) 866 867 868 ############################## 869 # Console list menu handling # 870 ############################## 871 872 def _console_list_menu_toggled(self, src, dev): 873 if not dev or dev.DEVICE_TYPE == "graphics": 874 self.widget("console-pages").set_current_page( 875 _CONSOLE_PAGE_GRAPHICS) 876 self.idle_add(self._init_viewer) 877 return 878 879 target_port = dev.get_xml_idx() 880 serial = None 881 name = src.get_label() 882 for s in self._serial_consoles: 883 if s.name == name: 884 serial = s 885 break 886 887 if not serial: 888 serial = vmmSerialConsole(self.vm, target_port, name) 889 serial.set_focus_callbacks(self._serial_focus_changed_cb, 890 self._serial_focus_changed_cb) 891 892 title = Gtk.Label(label=name) 893 self.widget("serial-pages").append_page(serial.get_box(), title) 894 self._serial_consoles.append(serial) 895 896 serial.open_console() 897 page_idx = self._serial_consoles.index(serial) 898 self.widget("console-pages").set_current_page(_CONSOLE_PAGE_SERIAL) 899 self.widget("serial-pages").set_current_page(page_idx) 900 901 def _populate_console_list_menu(self, ignore=None): 902 self._consolemenu.rebuild_menu( 903 self.vm, self._console_list_menu, 904 self._console_list_menu_toggled) 905 906 def _toggle_first_console_menu_item(self): 907 # We iterate through the 'console' menu and activate the first 908 # valid entry... hacky but it works 909 self._populate_console_list_menu() 910 found = self._consolemenu.activate_default(self._console_list_menu) 911 if not found: 912 # Calling this with dev=None will trigger _init_viewer 913 # which shows some meaningful errors 914 self._console_list_menu_toggled(None, None) 915 916 def _activate_default_console_page(self): 917 if self.vm.is_runable(): 918 self._show_vm_status_unavailable() 919 return 920 921 viewer_initialized = (self._viewer and self._viewer.console_is_open()) 922 if viewer_initialized: 923 return 924 925 cpage = self.widget("console-pages").get_current_page() 926 if cpage != _CONSOLE_PAGE_UNAVAILABLE: 927 return 928 929 # If we are in this condition it should mean the VM was 930 # just started, so connect to the default page 931 self._toggle_first_console_menu_item() 932 933 934 ################ 935 # UI listeners # 936 ################ 937 938 def _auth_login_cb(self, src): 939 self._set_credentials() 940 941 def _connect_button_clicked_cb(self, src): 942 self._viewer_connect_clicked = True 943 self._init_viewer() 944 945 def _page_changed_cb(self, src, origpage, newpage): 946 # Hide the contents of all other pages, so they don't screw 947 # up window sizing 948 for i in range(src.get_n_pages()): 949 src.get_nth_page(i).set_visible(i == newpage) 950 951 # Dispatch the next bit in idle_add, so the UI size can change 952 self.idle_emit("page-changed") 953 954 955 ########################### 956 # API used by vmmVMWindow # 957 ########################### 958 959 def vmwindow_viewer_has_usb_redirection(self): 960 return bool(self._viewer and 961 self._viewer.console_has_usb_redirection()) 962 def vmwindow_viewer_get_usb_widget(self): 963 return self._viewer.console_get_usb_widget() 964 def vmwindow_viewer_get_pixbuf(self): 965 return self._viewer.console_get_pixbuf() 966 967 def vmwindow_close(self): 968 return self._activate_vm_unavailable_page( 969 _("Viewer disconnected.")) 970 def vmwindow_get_title_message(self): 971 if self._pointer_is_grabbed and self._viewer: 972 keystr = self._viewer.console_get_grab_keys() 973 return _("Press %s to release pointer.") % keystr 974 975 def vmwindow_activate_default_console_page(self): 976 return self._activate_default_console_page() 977 def vmwindow_refresh_vm_state(self): 978 return self._refresh_vm_state() 979 980 def vmwindow_set_size_to_vm(self): 981 return self._set_size_to_vm() 982 def vmwindow_set_fullscreen(self, do_fullscreen): 983 self._change_fullscreen(do_fullscreen) 984 985 def vmwindow_get_keycombo_menu(self): 986 return self._keycombo_menu 987 def vmwindow_get_console_list_menu(self): 988 return self._console_list_menu 989 def vmwindow_get_viewer_is_visible(self): 990 return self._viewer_is_visible() 991 def vmwindow_get_can_usb_redirect(self): 992 return self._viewer_can_usb_redirect() 993 def vmwindow_get_resizeguest_tooltip(self): 994 return self._viewer_get_resizeguest_tooltip() 995 996 def vmwindow_sync_scaling_with_display(self): 997 return self._sync_scaling_with_display() 998 def vmwindow_sync_resizeguest_with_display(self): 999 return self._sync_resizeguest_with_display() 1000