1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2006-2008 Red Hat, Inc.
4
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
5
# Copyright (C) 2010 Marc-André Lureau <marcandre.lureau@redhat.com>
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 2 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
31
import SpiceClientGtk as spice
40
import virtManager.util as util
41
import virtManager.uihelpers as uihelpers
42
from virtManager.autodrawer import AutoDrawer
43
from virtManager.baseclass import vmmGObjectUI, vmmGObject
44
from virtManager.error import vmmErrorDialog
51
def has_property(obj, setting):
53
obj.get_property(setting)
65
def open(self, connhost, connuser, connport, gaddr, gport, gsocket):
66
if self.outfd is not None:
72
argv += ["-p", str(connport)]
75
argv += ['-l', connuser]
79
# Build 'nc' command run on the remote host
81
# This ugly thing is a shell script to detect availability of
82
# the -q option for 'nc': debian and suse based distros need this
83
# flag to ensure the remote nc will exit on EOF, so it will go away
84
# when we close the VNC tunnel. If it doesn't go away, subsequent
85
# VNC connection attempts will hang.
87
# Fedora's 'nc' doesn't have this option, and apparently defaults
88
# to the desired behavior.
91
nc_params = "-U %s" % gsocket
93
nc_params = "%s %s" % (gaddr, gport)
96
"""nc -q 2>&1 | grep -q "requires an argument";"""
97
"""if [ $? -eq 0 ] ; then"""
98
""" CMD="nc -q 0 %(nc_params)s";"""
100
""" CMD="nc %(nc_params)s";"""
103
{'nc_params': nc_params})
106
argv.append("'%s'" % nc_cmd)
108
argv_str = reduce(lambda x, y: x + " " + y, argv[1:])
109
logging.debug("Creating SSH tunnel: %s" % argv_str)
111
fds = socket.socketpair()
112
errorfds = socket.socketpair()
122
os.dup(fds[1].fileno())
123
os.dup(fds[1].fileno())
124
os.dup(errorfds[1].fileno())
131
logging.debug("Tunnel PID=%d OUTFD=%d ERRFD=%d" %
132
(pid, fds[0].fileno(), errorfds[0].fileno()))
133
errorfds[0].setblocking(0)
136
self.errfd = errorfds[0]
141
raise SystemError("can't open a new tunnel: fd=%d" % fd)
145
if self.outfd is None:
148
logging.debug("Shutting down tunnel PID=%d OUTFD=%d ERRFD=%d" %
149
(self.pid, self.outfd.fileno(),
150
self.errfd.fileno()))
156
os.kill(self.pid, signal.SIGKILL)
159
def get_err_output(self):
163
new = self.errfd.recv(1024)
175
class Tunnels(object):
176
def __init__(self, connhost, connuser, connport, gaddr, gport, gsocket):
177
self.connhost = connhost
178
self.connuser = connuser
179
self.connport = connport
183
self.gsocket = gsocket
188
fd = t.open(self.connhost, self.connuser, self.connport,
189
self.gaddr, self.gport, self.gsocket)
190
self._tunnels.append(t)
194
for l in self._tunnels:
197
def get_err_output(self):
199
for l in self._tunnels:
200
errout += l.get_err_output()
204
class Viewer(vmmGObject):
205
def __init__(self, console):
206
vmmGObject.__init__(self)
207
self.console = console
211
raise NotImplementedError()
217
self.display.destroy()
221
def get_widget(self):
224
def get_pixbuf(self):
225
return self.display.get_pixbuf()
227
def get_grab_keys_from_config(self):
229
grab_keys = self.config.get_keys_combination(True)
230
if grab_keys is not None:
231
# If somebody edited this in GConf it would fail so
232
# we encapsulate this into try/except block
234
keys = map(int, grab_keys.split(','))
236
logging.debug("Error in grab_keys configuration in GConf")
240
def get_grab_keys(self):
243
keys = self.display.get_grab_keys()
246
keystr = gtk.gdk.keyval_name(k)
248
keystr = keystr + "+" + gtk.gdk.keyval_name(k)
254
def send_keys(self, keys):
255
return self.display.send_keys(keys)
257
def set_grab_keys(self):
259
keys = self.get_grab_keys_from_config()
261
self.display.set_grab_keys(keys)
263
logging.debug("Error when getting the grab keys combination: %s" %
266
def open_host(self, host, user, port, socketpath, password=None):
267
raise NotImplementedError()
269
def get_desktop_resolution(self):
270
raise NotImplementedError()
272
class VNCViewer(Viewer):
273
def __init__(self, console):
274
Viewer.__init__(self, console)
275
self.display = gtkvnc.Display()
278
# Last noticed desktop resolution
279
self.desktop_resolution = None
281
def init_widget(self):
282
# Set default grab key combination if found and supported
283
if self.config.vnc_grab_keys_supported():
286
self.display.realize()
288
# Make sure viewer doesn't force resize itself
289
self.display.set_force_size(False)
291
self.console.refresh_scaling()
293
self.display.set_keyboard_grab(False)
294
self.display.set_pointer_grab(True)
296
self.display.connect("vnc-pointer-grab", self.console.pointer_grabbed)
297
self.display.connect("vnc-pointer-ungrab", self.console.pointer_ungrabbed)
298
self.display.connect("vnc-auth-credential", self._auth_credential)
299
self.display.connect("vnc-initialized",
300
lambda src: self.console.connected())
301
self.display.connect("vnc-disconnected",
302
lambda src: self.console.disconnected())
303
self.display.connect("vnc-desktop-resize", self._desktop_resize)
304
self.display.connect("focus-in-event", self.console.viewer_focus_changed)
305
self.display.connect("focus-out-event", self.console.viewer_focus_changed)
309
def _desktop_resize(self, src_ignore, w, h):
310
self.desktop_resolution = (w, h)
311
self.console.window.get_widget("console-vnc-scroll").queue_resize()
313
def get_desktop_resolution(self):
314
return self.desktop_resolution
316
def _auth_credential(self, src_ignore, credList):
317
for cred in credList:
318
if cred in [gtkvnc.CREDENTIAL_PASSWORD,
319
gtkvnc.CREDENTIAL_USERNAME,
320
gtkvnc.CREDENTIAL_CLIENTNAME]:
323
self.console.err.show_err(
324
summary=_("Unable to provide requested credentials to "
326
details=(_("The credential type %s is not supported") %
328
title=_("Unable to authenticate"),
331
# schedule_retry will error out
332
self.console.viewerRetriesScheduled = 10
334
self.console.activate_unavailable_page(
335
_("Unsupported console authentication type"))
340
for cred in credList:
341
logging.debug("Got credential request %s" % cred)
342
if cred == gtkvnc.CREDENTIAL_PASSWORD:
344
elif cred == gtkvnc.CREDENTIAL_USERNAME:
346
elif cred == gtkvnc.CREDENTIAL_CLIENTNAME:
347
self.display.set_credential(cred, "libvirt-vnc")
349
if withUsername or withPassword:
350
self.console.activate_auth_page(withPassword, withUsername)
352
def get_scaling(self):
353
return self.display.get_scaling()
355
def set_scaling(self, scaling):
356
return self.display.set_scaling(scaling)
367
return self.display.is_open()
369
def open_host(self, host, user, port, socketpath, password=None):
374
self.display.open_host(host, port)
378
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
379
sock.connect(socketpath)
382
raise RuntimeError(_("Error opening socket path '%s': %s") %
385
fd = self.sockfd.fileno()
387
raise RuntimeError((_("Error opening socket path '%s'") %
388
socketpath) + " fd=%s" % fd)
391
def open_fd(self, fd):
392
self.display.open_fd(fd)
394
def get_grab_keys(self):
396
if self.config.vnc_grab_keys_supported():
397
keystr = super(VNCViewer, self).get_grab_keys()
399
# If grab keys are set to None then preserve old behaviour since
400
# the GTK-VNC - we're using older version of GTK-VNC
402
keystr = "Control_L+Alt_L"
405
def set_credential_username(self, cred):
406
self.display.set_credential(gtkvnc.CREDENTIAL_USERNAME, cred)
408
def set_credential_password(self, cred):
409
self.display.set_credential(gtkvnc.CREDENTIAL_PASSWORD, cred)
412
class SpiceViewer(Viewer):
413
def __init__(self, console):
414
Viewer.__init__(self, console)
415
self.spice_session = None
418
self.display_channel = None
420
def _init_widget(self):
422
self.console.refresh_scaling()
424
self.display.realize()
425
self.display.connect("mouse-grab", lambda src, g: g and self.console.pointer_grabbed(src))
426
self.display.connect("mouse-grab", lambda src, g: g or self.console.pointer_ungrabbed(src))
430
if self.spice_session is not None:
431
self.spice_session.disconnect()
432
self.spice_session = None
435
self.display.destroy()
437
self.display_channel = None
440
return self.spice_session != None
442
def _main_channel_event_cb(self, channel, event):
443
if event == spice.CHANNEL_CLOSED:
445
self.console.disconnected()
446
elif event == spice.CHANNEL_ERROR_AUTH:
448
self.console.activate_auth_page()
450
def _channel_open_fd_request(self, channel, tls_ignore):
451
if not self.console.tunnels:
452
raise SystemError("Got fd request with no configured tunnel!")
454
fd = self.console.tunnels.open_new()
457
def _channel_new_cb(self, session, channel):
458
gobject.GObject.connect(channel, "open-fd",
459
self._channel_open_fd_request)
461
if type(channel) == spice.MainChannel:
462
channel.connect_after("channel-event", self._main_channel_event_cb)
465
if type(channel) == spice.DisplayChannel:
466
channel_id = channel.get_property("channel-id")
469
logging.debug("Spice multi-head unsupported")
472
self.display_channel = channel
473
self.display = spice.Display(self.spice_session, channel_id)
474
self.console.window.get_widget("console-vnc-viewport").add(self.display)
476
self.console.connected()
479
if (type(channel) in [spice.PlaybackChannel, spice.RecordChannel] and
481
self.audio = spice.Audio(self.spice_session)
484
def get_desktop_resolution(self):
485
if (not self.display_channel or
486
not has_property(self.display_channel, "width")):
488
return self.display_channel.get_properties("width", "height")
490
def open_host(self, host, user, port, socketpath, password=None):
494
uri += (user and str(user) or "")
495
uri += str(host) + "?port=" + str(port)
496
logging.debug("spice uri: %s" % uri)
498
self.spice_session = spice.Session()
499
self.spice_session.set_property("uri", uri)
501
self.spice_session.set_property("password", password)
502
gobject.GObject.connect(self.spice_session, "channel-new",
503
self._channel_new_cb)
504
self.spice_session.connect()
506
def open_fd(self, fd, password=None):
507
self.spice_session = spice.Session()
509
self.spice_session.set_property("password", password)
510
gobject.GObject.connect(self.spice_session, "channel-new",
511
self._channel_new_cb)
512
self.spice_session.open_fd(fd)
514
def set_credential_password(self, cred):
515
self.spice_session.set_property("password", cred)
516
if self.console.tunnels:
517
fd = self.console.tunnels.open_new()
518
self.spice_session.open_fd(fd)
520
self.spice_session.connect()
522
def get_scaling(self):
523
return self.display.get_property("resize-guest")
525
def set_scaling(self, scaling):
526
self.display.set_property("resize-guest", scaling)
529
class vmmConsolePages(vmmGObjectUI):
530
def __init__(self, vm, window):
531
vmmGObjectUI.__init__(self, None, None)
535
self.windowname = "vmm-details"
537
self.topwin = self.widget(self.windowname)
538
self.err = vmmErrorDialog(self.topwin)
540
self.pointer_is_grabbed = False
542
self.vm.connect("config-changed", self.change_title)
544
# State for disabling modifiers when keyboard is grabbed
545
self.accel_groups = gtk.accel_groups_from_object(self.topwin)
546
self.gtk_settings_accel = None
547
self.gtk_settings_mnemonic = None
549
# Initialize display widget
552
self.viewerRetriesScheduled = 0
553
self.viewerRetryDelay = 125
554
self._viewer_connected = False
555
self.viewer_connecting = False
556
self.scale_type = self.vm.get_console_scaling()
559
self.fs_toolbar = None
560
self.fs_drawer = None
561
self.keycombo_menu = uihelpers.build_keycombo_menu(self.send_key)
562
self.init_fs_toolbar()
564
finish_img = gtk.image_new_from_stock(gtk.STOCK_YES,
565
gtk.ICON_SIZE_BUTTON)
566
self.widget("console-auth-login").set_image(finish_img)
568
# Make viewer widget background always be black
569
black = gtk.gdk.Color(0, 0, 0)
570
self.widget("console-vnc-viewport").modify_bg(gtk.STATE_NORMAL,
573
# Signals are added by vmmDetails. Don't use signal_autoconnect here
574
# or it changes will be overwritten
575
# Set console scaling
576
self.add_gconf_handle(
577
self.vm.on_console_scaling_changed(self.refresh_scaling))
579
scroll = self.widget("console-vnc-scroll")
580
scroll.connect("size-allocate", self.scroll_size_allocate)
581
self.add_gconf_handle(
582
self.config.on_console_accels_changed(self.set_enable_accel))
586
def is_visible(self):
587
if self.topwin.flags() & gtk.VISIBLE:
595
self.viewer.cleanup()
598
self.keycombo_menu.destroy()
599
self.keycombo_menu = None
600
self.fs_toolbar.destroy()
601
self.fs_toolbar = None
602
self.fs_drawer.destroy()
603
self.fs_drawer = None
605
##########################
606
# Initialization helpers #
607
##########################
609
def init_fs_toolbar(self):
610
scroll = self.widget("console-vnc-scroll")
611
pages = self.widget("console-pages")
614
self.fs_toolbar = gtk.Toolbar()
615
self.fs_toolbar.set_show_arrow(False)
616
self.fs_toolbar.set_no_show_all(True)
617
self.fs_toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
619
# Exit fullscreen button
620
button = gtk.ToolButton(gtk.STOCK_LEAVE_FULLSCREEN)
621
util.tooltip_wrapper(button, _("Leave fullscreen"))
623
self.fs_toolbar.add(button)
624
button.connect("clicked", self.leave_fullscreen)
626
def keycombo_menu_clicked(src):
628
def menu_location(menu, toolbar):
630
x, y = toolbar.window.get_origin()
631
ignore, height = toolbar.window.get_size()
633
return x, y + height, True
635
self.keycombo_menu.popup(None, None, menu_location, 0,
636
gtk.get_current_event_time(),
639
item = gtk.ToolButton()
640
item.set_icon_name("preferences-desktop-keyboard-shortcuts")
641
util.tooltip_wrapper(item, _("Send key combination"))
643
item.connect("clicked", keycombo_menu_clicked)
644
self.fs_toolbar.add(item)
646
self.fs_drawer = AutoDrawer()
647
self.fs_drawer.set_active(False)
648
self.fs_drawer.set_over(self.fs_toolbar)
649
self.fs_drawer.set_under(scroll)
650
self.fs_drawer.set_offset(-1)
651
self.fs_drawer.set_fill(False)
652
self.fs_drawer.set_overlap_pixels(1)
653
self.fs_drawer.set_nooverlap_pixels(0)
654
self.fs_drawer.show_all()
656
pages.add(self.fs_drawer)
658
def change_title(self, ignore1=None):
659
title = self.vm.get_name() + " " + _("Virtual Machine")
661
if self.pointer_is_grabbed and self.viewer:
662
keystr = self.viewer.get_grab_keys()
663
keymsg = _("Press %s to release pointer.") % keystr
665
title = keymsg + " " + title
667
self.topwin.set_title(title)
669
def viewer_focus_changed(self, ignore1=None, ignore2=None):
670
has_focus = self.viewer and self.viewer.get_widget() and \
671
self.viewer.get_widget().get_property("has-focus")
672
force_accel = self.config.get_console_accels()
675
self._enable_modifiers()
676
elif has_focus and self.viewer_connected:
677
self._disable_modifiers()
679
self._enable_modifiers()
681
def pointer_grabbed(self, src_ignore):
682
self.pointer_is_grabbed = True
685
def pointer_ungrabbed(self, src_ignore):
686
self.pointer_is_grabbed = False
689
def _disable_modifiers(self):
690
if self.gtk_settings_accel is not None:
693
for g in self.accel_groups:
694
self.topwin.remove_accel_group(g)
696
settings = gtk.settings_get_default()
697
self.gtk_settings_accel = settings.get_property('gtk-menu-bar-accel')
698
settings.set_property('gtk-menu-bar-accel', None)
700
if has_property(settings, "gtk-enable-mnemonics"):
701
self.gtk_settings_mnemonic = settings.get_property(
702
"gtk-enable-mnemonics")
703
settings.set_property("gtk-enable-mnemonics", False)
705
def _enable_modifiers(self):
706
if self.gtk_settings_accel is None:
709
settings = gtk.settings_get_default()
710
settings.set_property('gtk-menu-bar-accel', self.gtk_settings_accel)
711
self.gtk_settings_accel = None
713
if self.gtk_settings_mnemonic is not None:
714
settings.set_property("gtk-enable-mnemonics",
715
self.gtk_settings_mnemonic)
717
for g in self.accel_groups:
718
self.topwin.add_accel_group(g)
720
def set_enable_accel(self, ignore=None, ignore1=None,
721
ignore2=None, ignore3=None):
722
# Make sure modifiers are up to date
723
self.viewer_focus_changed()
725
def refresh_scaling(self, ignore1=None, ignore2=None, ignore3=None,
727
self.scale_type = self.vm.get_console_scaling()
728
self.widget("details-menu-view-scale-always").set_active(
729
self.scale_type == self.config.CONSOLE_SCALE_ALWAYS)
730
self.widget("details-menu-view-scale-never").set_active(
731
self.scale_type == self.config.CONSOLE_SCALE_NEVER)
732
self.widget("details-menu-view-scale-fullscreen").set_active(
733
self.scale_type == self.config.CONSOLE_SCALE_FULLSCREEN)
735
self.update_scaling()
737
def set_scale_type(self, src):
738
if not src.get_active():
741
if src == self.widget("details-menu-view-scale-always"):
742
self.scale_type = self.config.CONSOLE_SCALE_ALWAYS
743
elif src == self.widget("details-menu-view-scale-fullscreen"):
744
self.scale_type = self.config.CONSOLE_SCALE_FULLSCREEN
745
elif src == self.widget("details-menu-view-scale-never"):
746
self.scale_type = self.config.CONSOLE_SCALE_NEVER
748
self.vm.set_console_scaling(self.scale_type)
749
self.update_scaling()
751
def update_scaling(self):
755
curscale = self.viewer.get_scaling()
756
fs = self.widget("control-fullscreen").get_active()
757
vnc_scroll = self.widget("console-vnc-scroll")
759
if (self.scale_type == self.config.CONSOLE_SCALE_NEVER
760
and curscale == True):
761
self.viewer.set_scaling(False)
762
elif (self.scale_type == self.config.CONSOLE_SCALE_ALWAYS
763
and curscale == False):
764
self.viewer.set_scaling(True)
765
elif (self.scale_type == self.config.CONSOLE_SCALE_FULLSCREEN
767
self.viewer.set_scaling(fs)
769
# Refresh viewer size
770
vnc_scroll.queue_resize()
772
def auth_login(self, ignore):
773
self.set_credentials()
774
self.activate_viewer_page()
776
def toggle_fullscreen(self, src):
777
do_fullscreen = src.get_active()
778
self._change_fullscreen(do_fullscreen)
780
def leave_fullscreen(self, ignore=None):
781
self._change_fullscreen(False)
783
def _change_fullscreen(self, do_fullscreen):
784
self.widget("control-fullscreen").set_active(do_fullscreen)
787
self.topwin.fullscreen()
788
self.fs_toolbar.show()
789
self.fs_drawer.set_active(True)
790
self.widget("toolbar-box").hide()
791
self.widget("details-menubar").hide()
793
self.fs_toolbar.hide()
794
self.fs_drawer.set_active(False)
795
self.topwin.unfullscreen()
797
if self.widget("details-menu-view-toolbar").get_active():
798
self.widget("toolbar-box").show()
799
self.widget("details-menubar").show()
801
self.update_scaling()
803
def size_to_vm(self, src_ignore):
804
# Resize the console to best fit the VM resolution
807
if not self.viewer.get_desktop_resolution():
810
w, h = self.viewer.get_desktop_resolution()
811
self.topwin.unmaximize()
812
self.topwin.resize(1, 1)
813
self.queue_scroll_resize_helper(w, h)
815
def send_key(self, src, keys):
819
self.viewer.send_keys(keys)
822
##########################
823
# State tracking methods #
824
##########################
826
def view_vm_status(self):
827
status = self.vm.status()
828
if status == libvirt.VIR_DOMAIN_SHUTOFF:
829
self.activate_unavailable_page(_("Guest not running"))
831
if status == libvirt.VIR_DOMAIN_CRASHED:
832
self.activate_unavailable_page(_("Guest has crashed"))
834
def close_viewer(self):
835
viewport = self.widget("console-vnc-viewport")
836
if self.viewer is None:
839
v = self.viewer # close_viewer() can be reentered
843
if w and w in viewport.get_children():
848
self.viewer_connected = False
849
self.leave_fullscreen()
851
def update_widget_states(self, vm, status_ignore):
852
runable = vm.is_runable()
853
pages = self.widget("console-pages")
854
page = pages.get_current_page()
857
if page != PAGE_UNAVAILABLE:
858
pages.set_current_page(PAGE_UNAVAILABLE)
860
self.view_vm_status()
863
elif page in [PAGE_UNAVAILABLE, PAGE_VIEWER]:
864
if self.viewer and self.viewer.is_open():
865
self.activate_viewer_page()
867
self.viewerRetriesScheduled = 0
868
self.viewerRetryDelay = 125
877
def activate_unavailable_page(self, msg):
879
This function is passed to serialcon.py at least, so change
883
self.widget("console-pages").set_current_page(PAGE_UNAVAILABLE)
884
self.widget("details-menu-vm-screenshot").set_sensitive(False)
885
self.widget("console-unavailable").set_label("<b>" + msg + "</b>")
887
def activate_auth_page(self, withPassword=True, withUsername=False):
888
(pw, username) = self.config.get_console_password(self.vm)
889
self.widget("details-menu-vm-screenshot").set_sensitive(False)
892
self.widget("console-auth-password").show()
893
self.widget("label-auth-password").show()
895
self.widget("console-auth-password").hide()
896
self.widget("label-auth-password").hide()
899
self.widget("console-auth-username").show()
900
self.widget("label-auth-username").show()
902
self.widget("console-auth-username").hide()
903
self.widget("label-auth-username").hide()
905
self.widget("console-auth-username").set_text(username)
906
self.widget("console-auth-password").set_text(pw)
908
if self.config.has_keyring():
909
self.widget("console-auth-remember").set_sensitive(True)
910
if pw != "" or username != "":
911
self.widget("console-auth-remember").set_active(True)
913
self.widget("console-auth-remember").set_active(False)
915
self.widget("console-auth-remember").set_sensitive(False)
916
self.widget("console-pages").set_current_page(PAGE_AUTHENTICATE)
918
self.widget("console-auth-username").grab_focus()
920
self.widget("console-auth-password").grab_focus()
923
def activate_viewer_page(self):
924
self.widget("console-pages").set_current_page(PAGE_VIEWER)
925
self.widget("details-menu-vm-screenshot").set_sensitive(True)
926
if self.viewer and self.viewer.get_widget():
927
self.viewer.get_widget().grab_focus()
929
def page_changed(self, ignore1=None, ignore2=None, ignore3=None):
930
self.set_allow_fullscreen()
932
def set_allow_fullscreen(self):
933
cpage = self.widget("console-pages").get_current_page()
934
dpage = self.widget("details-pages").get_current_page()
936
allow_fullscreen = (dpage == 0 and
937
cpage == PAGE_VIEWER and
938
self.viewer_connected)
940
self.widget("control-fullscreen").set_sensitive(allow_fullscreen)
941
self.widget("details-menu-view-fullscreen").set_sensitive(allow_fullscreen)
943
def disconnected(self):
945
if self.tunnels is not None:
946
errout = self.tunnels.get_err_output()
947
self.tunnels.close_all()
951
logging.debug("Viewer disconnected")
953
# Make sure modifiers are set correctly
954
self.viewer_focus_changed()
956
if (self.skip_connect_attempt() or
957
self.guest_not_avail()):
958
# Exit was probably for legitimate reasons
959
self.view_vm_status()
962
error = _("Error: viewer connection to hypervisor host got refused "
965
logging.debug("Error output from closed console: %s" % errout)
966
error += "\n\nError: %s" % errout
968
self.activate_unavailable_page(error)
970
def _set_viewer_connected(self, val):
971
self._viewer_connected = val
972
self.set_allow_fullscreen()
973
def _get_viewer_connected(self):
974
return self._viewer_connected
975
viewer_connected = property(_get_viewer_connected, _set_viewer_connected)
978
self.viewer_connected = True
979
logging.debug("Viewer connected")
980
self.activate_viewer_page()
982
# Had a succesfull connect, so reset counters now
983
self.viewerRetriesScheduled = 0
984
self.viewerRetryDelay = 125
986
# Make sure modifiers are set correctly
987
self.viewer_focus_changed()
989
def schedule_retry(self):
990
if self.viewerRetriesScheduled >= 10:
991
logging.error("Too many connection failures, not retrying again")
994
self.safe_timeout_add(self.viewerRetryDelay, self.try_login)
996
if self.viewerRetryDelay < 2000:
997
self.viewerRetryDelay = self.viewerRetryDelay * 2
999
def skip_connect_attempt(self):
1000
return (self.viewer or
1001
not self.is_visible())
1003
def guest_not_avail(self):
1004
return (self.vm.is_shutoff() or self.vm.is_crashed())
1006
def try_login(self, src_ignore=None):
1007
if self.viewer_connecting:
1011
self.viewer_connecting = True
1014
self.viewer_connecting = False
1016
def _try_login(self):
1017
if self.skip_connect_attempt():
1018
# Don't try and login for these cases
1021
if self.guest_not_avail():
1022
# Guest isn't running, schedule another try
1023
self.activate_unavailable_page(_("Guest not running"))
1024
self.schedule_retry()
1028
(protocol, transport,
1029
connhost, connuser, connport,
1030
gaddr, gport, gsocket) = self.vm.get_graphics_console()
1031
except Exception, e:
1032
# We can fail here if VM is destroyed: xen is a bit racy
1033
# and can't handle domain lookups that soon after
1034
logging.exception("Getting graphics console failed: %s" % str(e))
1037
if protocol is None:
1038
logging.debug("No graphics configured for guest")
1039
self.activate_unavailable_page(
1040
_("Graphical console not configured for guest"))
1043
if protocol not in self.config.embeddable_graphics():
1044
logging.debug("Don't know how to show graphics type '%s' "
1045
"disabling console page" % protocol)
1047
msg = (_("Cannot display graphical console type '%s'")
1049
if protocol == "spice":
1050
msg += ":\n %s" % self.config.get_spice_error()
1052
self.activate_unavailable_page(msg)
1055
if (gport == -1 and not gsocket):
1056
self.activate_unavailable_page(
1057
_("Graphical console is not yet active for guest"))
1058
self.schedule_retry()
1061
self.activate_unavailable_page(
1062
_("Connecting to graphical console for guest"))
1064
logging.debug("Starting connect process for "
1065
"proto=%s trans=%s connhost=%s connuser=%s "
1066
"connport=%s gaddr=%s gport=%s gsocket=%s" %
1067
(protocol, transport, connhost, connuser, connport,
1068
gaddr, gport, gsocket))
1070
if protocol == "vnc":
1071
self.viewer = VNCViewer(self)
1072
self.widget("console-vnc-viewport").add(
1073
self.viewer.get_widget())
1074
self.viewer.init_widget()
1075
elif protocol == "spice":
1076
self.viewer = SpiceViewer(self)
1078
self.set_enable_accel()
1080
if transport in ("ssh", "ext"):
1082
# Tunnel already open, no need to continue
1085
self.tunnels = Tunnels(connhost, connuser, connport,
1086
gaddr, gport, gsocket)
1087
fd = self.tunnels.open_new()
1089
self.viewer.open_fd(fd)
1092
self.viewer.open_host(connhost, connuser,
1093
str(gport), gsocket)
1095
except Exception, e:
1096
logging.exception("Error connection to graphical console")
1097
self.activate_unavailable_page(
1098
_("Error connecting to graphical console") + ":\n%s" % e)
1100
def set_credentials(self, src_ignore=None):
1101
passwd = self.widget("console-auth-password")
1102
if passwd.flags() & gtk.VISIBLE:
1103
self.viewer.set_credential_password(passwd.get_text())
1104
username = self.widget("console-auth-username")
1105
if username.flags() & gtk.VISIBLE:
1106
self.viewer.set_credential_username(username.get_text())
1108
if self.widget("console-auth-remember").get_active():
1109
self.config.set_console_password(self.vm, passwd.get_text(),
1110
username.get_text())
1112
def queue_scroll_resize_helper(self, w, h):
1114
Resize the VNC container widget to the requested size. The new size
1115
isn't a hard requirment so the user can still shrink the window
1116
again, as opposed to set_size_request
1118
widget = self.widget("console-vnc-scroll")
1121
def restore_scroll(src):
1122
is_scale = self.viewer.get_scaling()
1125
w_policy = gtk.POLICY_NEVER
1126
h_policy = gtk.POLICY_NEVER
1128
w_policy = gtk.POLICY_AUTOMATIC
1129
h_policy = gtk.POLICY_AUTOMATIC
1131
src.set_policy(w_policy, h_policy)
1135
src.queue_resize_no_redraw()
1136
self.safe_idle_add(restore_scroll, src)
1139
def request_cb(src, req):
1140
signal_id = signal_holder[0]
1144
src.disconnect(signal_id)
1146
self.safe_idle_add(unset_cb, widget)
1149
# Disable scroll bars while we resize, since resizing to the VM's
1150
# dimensions can erroneously show scroll bars when they aren't needed
1151
widget.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
1153
signal_id = widget.connect("size-request", request_cb)
1154
signal_holder.append(signal_id)
1156
widget.queue_resize()
1158
def scroll_size_allocate(self, src_ignore, req):
1159
if not self.viewer or not self.viewer.get_desktop_resolution():
1162
scroll = self.widget("console-vnc-scroll")
1163
is_scale = self.viewer.get_scaling()
1167
align_ratio = float(req.width) / float(req.height)
1169
desktop_w, desktop_h = self.viewer.get_desktop_resolution()
1172
desktop_ratio = float(desktop_w) / float(desktop_h)
1175
# Scaling disabled is easy, just force the VNC widget size. Since
1176
# we are inside a scrollwindow, it shouldn't cause issues.
1177
scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1178
self.viewer.get_widget().set_size_request(desktop_w, desktop_h)
1181
# Make sure we never show scrollbars when scaling
1182
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
1184
# Make sure there is no hard size requirement so we can scale down
1185
self.viewer.get_widget().set_size_request(-1, -1)
1187
# Make sure desktop aspect ratio is maintained
1188
if align_ratio > desktop_ratio:
1189
desktop_w = int(req.height * desktop_ratio)
1190
desktop_h = req.height
1191
dx = (req.width - desktop_w) / 2
1194
desktop_w = req.width
1195
desktop_h = int(req.width / desktop_ratio)
1196
dy = (req.height - desktop_h) / 2
1198
viewer_alloc = gtk.gdk.Rectangle(x=dx,
1203
self.viewer.get_widget().size_allocate(viewer_alloc)
1205
vmmGObjectUI.type_register(vmmConsolePages)