/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Drawing.Drawing2D; using System.Linq; using System.Reflection; using System.Windows.Forms; using System.Drawing; using System.ComponentModel; using System.Net; using System.Threading; using DotNetVnc; using log4net; using XenAdmin.Controls.ConsoleTab; using XenAPI; using XenAdmin.Core; using System.IO; using XenAdmin.Dialogs; using System.Security.Cryptography; using XenAdmin.Network; using XenCenterLib; using Console = XenAPI.Console; using Message = System.Windows.Forms.Message; using Timer = System.Threading.Timer; namespace XenAdmin.ConsoleView { public class XSVNCScreen : UserControl { private const int SHORT_RETRY_COUNT = 10; private const int SHORT_RETRY_SLEEP_TIME = 100; private const int RETRY_SLEEP_TIME = 5000; private const int RDP_POLL_INTERVAL = 30000; public const int RDP_PORT = 3389; private const int VNC_PORT = 5900; private const int CONSOLE_SIZE_OFFSET = 6; private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private int ConnectionRetries = 0; private volatile bool useVNC = true; private bool autoCaptureKeyboardAndMouse = true; private readonly Color focusColor = SystemColors.MenuHighlight; /// /// May only be written on the event thread. May be read off the event thread, to check whether /// the VNC source has been switched during connection. /// private volatile VNCGraphicsClient vncClient = null; private RdpClient rdpClient; private Timer connectionPoller = null; private VM sourceVM = null; private bool sourceIsPV = false; private readonly Object hostedConsolesLock = new Object(); private List> hostedConsoles = null; /// /// This is assigned when the hosted connection connects up. It's used by PollPort to check for /// the IP address from the guest metrics, so for the lifetime of that hosted connection, we can /// poll for the in-guest VNC using the same session. activeSession must be accessed only under /// the activeSessionLock. /// private Session activeSession = null; private readonly Object activeSessionLock = new Object(); /// /// Xvnc will blacklist us if we're too quick with the disconnect and reconnect that we do /// when polling for the VNC port. To get around this, we keep the connection open between poll /// and proper connection. pendingVNCConnection must be accessed only under the /// pendingVNCConnectionLock. Work under this lock must be non-blocking, because it's used on /// Dispose. /// private Stream pendingVNCConnection = null; private readonly Object pendingVNCConnectionLock = new Object(); internal EventHandler ResizeHandler; public event EventHandler UserCancelledAuth; public event EventHandler VncConnectionAttemptCancelled; public event Action GpuStatusChanged; public event Action ConnectionNameChanged; public bool RdpVersionWarningNeeded { get { return rdpClient != null && rdpClient.needsRdpVersionWarning; }} internal readonly VNCTabView parentVNCTabView; [DefaultValue(false)] public bool UserWantsToSwitchProtocol { get; set; } private bool hasRDP { get { return Source != null && Source.HasRDP(); } } /// /// Whether we have tried to login without providing a password (covers the case where the user /// has configured VNC not to require a login password). If no password is saved, passwordless /// login is tried once. /// private bool haveTriedLoginWithoutPassword = false; private bool ignoreNextError = false; private Dictionary cachedNetworks; /// /// The last known VNC password for this VM. /// private char[] vncPassword = null; internal ConsoleKeyHandler KeyHandler; internal string ElevatedUsername; internal string ElevatedPassword; internal XSVNCScreen(VM source, EventHandler resizeHandler, VNCTabView parent, string elevatedUsername, string elevatedPassword) : base() { this.ResizeHandler = resizeHandler; this.parentVNCTabView = parent; this.Source = source; this.KeyHandler = parentVNCTabView.KeyHandler; ElevatedUsername = elevatedUsername; ElevatedPassword = elevatedPassword; #pragma warning disable 0219 IntPtr _ = Handle; #pragma warning restore 0219 initSubControl(); //We're going to try and catch when the IP address changes for the VM, and re-scan for ports. if (source == null) return; Properties.Settings.Default.PropertyChanged += Default_PropertyChanged; VM_guest_metrics guestMetrics = Source.Connection.Resolve(Source.guest_metrics); if (guestMetrics == null) return; cachedNetworks = guestMetrics.networks; guestMetrics.PropertyChanged += new PropertyChangedEventHandler(guestMetrics_PropertyChanged); } void Default_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "EnableRDPPolling") Program.Invoke(this, startPolling); } private void UnregisterEventListeners() { if (Source == null) return; Source.PropertyChanged -= new PropertyChangedEventHandler(VM_PropertyChanged); VM_guest_metrics guestMetrics = Source.Connection.Resolve(Source.guest_metrics); if (guestMetrics == null) return; guestMetrics.PropertyChanged -= new PropertyChangedEventHandler(guestMetrics_PropertyChanged); } void guestMetrics_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (Source == null) return; if (e.PropertyName == "networks") { Dictionary newNetworks = (sender as VM_guest_metrics).networks; if (!equateDictionary(newNetworks, cachedNetworks)) { Log.InfoFormat("Detected IP address change in vm {0}, repolling for VNC/RDP...", Source.Name()); cachedNetworks = newNetworks; Program.Invoke(this, startPolling); } } } private static bool equateDictionary(Dictionary d1, Dictionary d2) where S : IEquatable { if (d1.Count != d2.Count) return false; foreach (T key in d1.Keys) { if (!d2.ContainsKey(key) || !d2[key].Equals(d1[key])) return false; } return true; } private bool wasPaused = true; public void Pause() { if (RemoteConsole != null) { wasPaused = true; RemoteConsole.Pause(); } } public void Unpause() { if (RemoteConsole != null) { wasPaused = false; RemoteConsole.Unpause(); } } public Size DesktopSize { get { return RemoteConsole != null ? RemoteConsole.DesktopSize : Size.Empty; } } /// /// Nothrow guarantee. /// protected override void Dispose(bool disposing) { UnregisterEventListeners(); if (disposing) { if (connectionPoller != null) { connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); connectionPoller.Dispose(); connectionPoller = null; } if (RemoteConsole != null) { RemoteConsole.DisconnectAndDispose(); RemoteConsole = null; } Log.DebugFormat("Set Pending Vnc connection to null"); SetPendingVNCConnection(null); } base.Dispose(disposing); } public MethodInvoker OnDetectRDP = null; public MethodInvoker OnDetectVNC = null; public String rdpIP = null; public String vncIP = null; private void PollRDPPort(Object Sender) { if (hasRDP && !Properties.Settings.Default.EnableRDPPolling) { if (connectionPoller != null) connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); if (OnDetectRDP != null) Program.Invoke(this, OnDetectRDP); } else { rdpIP = null; String openIP = PollPort(RDP_PORT, false); if (openIP != null) { rdpIP = openIP; if (connectionPoller != null) connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); if (OnDetectRDP != null) Program.Invoke(this, OnDetectRDP); } } } private void PollVNCPort(Object Sender) { vncIP = null; String openIP = PollPort(VNC_PORT, true); if (openIP != null) { vncIP = openIP; if (connectionPoller != null) connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); if (OnDetectVNC != null) Program.Invoke(this, OnDetectVNC); } } /// /// scan each ip address (from the guest agent) for an open port /// public String PollPort(int port, bool vnc) { try { if (Source == null) return null; VM vm = Source; XenRef guestMetricsRef = vm.guest_metrics; if (guestMetricsRef == null || Helper.IsNullOrEmptyOpaqueRef(guestMetricsRef.opaque_ref)) return null; VM_guest_metrics metrics = vm.Connection.Resolve(guestMetricsRef); if (metrics == null) return null; Dictionary networks = metrics.networks; if (networks == null) return null; List ipAddresses = new List(); List ipv6Addresses = new List(); List ipAddressesForNetworksWithoutPifs = new List(); List ipv6AddressesForNetworksWithoutPifs = new List(); foreach (VIF vif in vm.Connection.ResolveAll(vm.VIFs)) { XenAPI.Network network = vif.Connection.Resolve(vif.network); Host host = vm.Connection.Resolve(vm.resident_on); PIF pif = Helpers.FindPIF(network, host); foreach (var networkInfo in networks.Where(n => n.Key.StartsWith(String.Format("{0}/ip", vif.device)))) { if (networkInfo.Key.EndsWith("ip") || networkInfo.Key.Contains("ipv4")) // IPv4 address { if (pif == null) ipAddressesForNetworksWithoutPifs.Add(networkInfo.Value); else if (pif.LinkStatus() == PIF.LinkState.Connected) ipAddresses.Add(networkInfo.Value); } else { if (networkInfo.Key.Contains("ipv6")) // IPv6 address, enclose in square brackets { if (pif == null) ipv6AddressesForNetworksWithoutPifs.Add(String.Format("[{0}]", networkInfo.Value)); else if (pif.LinkStatus() == PIF.LinkState.Connected) ipv6Addresses.Add(String.Format("[{0}]", networkInfo.Value)); } else continue; } } } ipAddresses = ipAddresses.Distinct().ToList(); ipAddresses.AddRange(ipv6Addresses); // make sure IPv4 addresses are scanned first (CA-102755) // add IP addresses for networks without PIFs ipAddresses.AddRange(ipAddressesForNetworksWithoutPifs); ipAddresses.AddRange(ipv6AddressesForNetworksWithoutPifs); foreach (String ipAddress in ipAddresses) { try { Log.DebugFormat("Poll port {0}:{1}", ipAddress, port); Stream s = connectGuest(ipAddress, port, vm.Connection); if (vnc) { Log.DebugFormat("Connected. Set Pending Vnc connection {0}:{1}", ipAddress, port); SetPendingVNCConnection(s); } else { s.Close(); } return ipAddress; } catch (Exception exn) { Log.Debug(exn); } } } catch (WebException) { // xapi has gone away. } catch (IOException) { // xapi has gone away. } catch (Failure exn) { if (exn.ErrorDescription[0] == Failure.HANDLE_INVALID) { // HANDLE_INVALID is fine -- the guest metrics are not there yet. } else if (exn.ErrorDescription[0] == Failure.SESSION_INVALID) { // SESSION_INVALID is fine -- these will expire from time to time. // We need to invalidate the session though. lock (activeSessionLock) { activeSession = null; } } else { Log.Warn("Exception while polling VM for port " + port + ".", exn); } } catch (Exception e) { Log.Warn("Exception while polling VM for port " + port + ".", e); } return null; } /// /// Exchange the current value of pendingVNCConnection with the given one, and close the old /// connection, if any. /// Nothrow guarantee. /// /// May be null private void SetPendingVNCConnection(Stream s) { Stream old_pending; lock (pendingVNCConnectionLock) { old_pending = pendingVNCConnection; pendingVNCConnection = s; } if (old_pending != null) { try { old_pending.Close(); } catch (Exception) { } } } private bool scaling; public bool Scaling { get { Program.AssertOnEventThread(); return scaling; } set { Program.AssertOnEventThread(); scaling = value; if (RemoteConsole != null) RemoteConsole.Scaling = value; } } public IRemoteConsole RemoteConsole { get { return vncClient != null ? (IRemoteConsole)vncClient : rdpClient; } set { if (vncClient != null) vncClient = (VNCGraphicsClient) value ; else if (rdpClient != null) rdpClient = (RdpClient)value; } } /// /// Creates the actual VNC or RDP client control. /// private void initSubControl() { Program.AssertOnEventThread(); //When switch to RDP from VNC, if RDP IP is empty, do not try to switch. if (String.IsNullOrEmpty(rdpIP) && !UseVNC && RemoteConsole != null) return; bool wasFocused = false; this.Controls.Clear(); //console size with some offset to accomodate focus rectangle Size currentConsoleSize = new Size(this.Size.Width - CONSOLE_SIZE_OFFSET, this.Size.Height - CONSOLE_SIZE_OFFSET); // Kill the old client. if (RemoteConsole != null) { wasFocused = RemoteConsole.ConsoleControl != null && RemoteConsole.ConsoleControl.Focused; RemoteConsole.DisconnectAndDispose(); RemoteConsole = null; this.vncPassword = null; } // Reset haveTriedLoginWithoutPassword = false; if (UseVNC || String.IsNullOrEmpty(rdpIP)) { this.AutoScroll = false; this.AutoScrollMinSize = new Size(0, 0); vncClient = new VNCGraphicsClient(this); vncClient.UseSource = UseSource; vncClient.DesktopResized += ResizeHandler; vncClient.Resize += ResizeHandler; vncClient.ErrorOccurred += ErrorHandler; vncClient.ConnectionSuccess += ConnectionSuccess; vncClient.Dock = DockStyle.Fill; } else { if (rdpClient == null) { if (this.ParentForm is FullScreenForm) currentConsoleSize = ((FullScreenForm)ParentForm).GetContentSize(); this.AutoScroll = true; this.AutoScrollMinSize = oldSize; rdpClient = new RdpClient(this, currentConsoleSize, ResizeHandler); rdpClient.OnDisconnected += new EventHandler(parentVNCTabView.RdpDisconnectedHandler); } } if (RemoteConsole != null && RemoteConsole.ConsoleControl != null) { RemoteConsole.KeyHandler = this.KeyHandler; RemoteConsole.SendScanCodes = !this.sourceIsPV; RemoteConsole.Scaling = Scaling; RemoteConsole.DisplayBorder = this.displayFocusRectangle; SetKeyboardAndMouseCapture(autoCaptureKeyboardAndMouse); if (wasPaused) RemoteConsole.Pause(); else RemoteConsole.Unpause(); ConnectToRemoteConsole(); if (wasFocused) RemoteConsole.Activate(); } if (GpuStatusChanged != null) GpuStatusChanged(MustConnectRemoteDesktop()); } internal bool MustConnectRemoteDesktop() { return (UseVNC || string.IsNullOrEmpty(rdpIP)) && Source.HasGPUPassthrough() && Source.power_state == vm_power_state.Running; } private void SetKeyboardAndMouseCapture(bool value) { if (RemoteConsole != null && RemoteConsole.ConsoleControl != null) RemoteConsole.ConsoleControl.TabStop = value; } private void ConnectToRemoteConsole() { if (vncClient != null) ThreadPool.QueueUserWorkItem(new WaitCallback(Connect), new KeyValuePair(vncClient, null)); else if (rdpClient != null) rdpClient.Connect(rdpIP); } void ConnectionSuccess(object sender, EventArgs e) { ConnectionRetries = 0; if (AutoSwitchRDPLater) { if (OnDetectRDP != null) Program.Invoke(this, OnDetectRDP); AutoSwitchRDPLater = false; } if (Helpers.CreamOrGreater(Source.Connection) && parentVNCTabView.IsRDPControlEnabled()) parentVNCTabView.EnableToggleVNCButton(); } internal bool AutoSwitchRDPLater { get; set; } internal bool UseVNC { get { return useVNC; } set { if (value != useVNC) { ConnectionRetries = 0; useVNC = value; scaling = false; initSubControl(); // Check if we have really switched. If not, change useVNC back (CA-102755) bool switched = true; if (useVNC) // we wanted VNC { if (vncClient == null && rdpClient != null) // it is actually RDP switched = false; } else // we wanted RDP { if (rdpClient == null && vncClient != null) // it is actually VNC switched = false; } if (!switched) { useVNC = !useVNC; } } } } private volatile bool useSource = true; /// /// Indicates whether to use the source or the detected vncIP /// public bool UseSource { get { return useSource; } set { if (value != useSource) { useSource = value; ConnectionRetries = 0; initSubControl(); } } } private VM Source { get { return this.sourceVM; } set { if (connectionPoller != null) { connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); connectionPoller = null; } if (sourceVM != null) { sourceVM.PropertyChanged -= new PropertyChangedEventHandler(VM_PropertyChanged); sourceVM = null; } sourceVM = value; if (value != null) { value.PropertyChanged += new PropertyChangedEventHandler(VM_PropertyChanged); sourceIsPV = !value.IsHVM(); startPolling(); lock (hostedConsolesLock) { hostedConsoles = Source.consoles; } VM_PropertyChanged(value, new PropertyChangedEventArgs("consoles")); } } } public string ConnectionName { get { if (Source == null) return null; if (Source.IsControlDomainZero()) return string.Format(Messages.CONSOLE_HOST, Source.AffinityServerString()); if (Source.is_control_domain) return string.Format(Messages.CONSOLE_HOST_NUTANIX, Source.AffinityServerString()); return Source.Name(); } } private bool InDefaultConsole() { // Windows VMs: // - UseVNC indicates whether to use the default desktop (true) or the remote desktop (false); // - UseSource is true by default and not used // Linux VMs: // - UseVNC is true by default and not used; // - UseSource indicates whether to use the text console (true) or the graphical vnc console (false); return UseVNC && UseSource; } private void startPolling() { //Disable the button first, but only if in text/default console (to allow user to return to the text console - ref. CA-70314) if (InDefaultConsole()) { parentVNCTabView.DisableToggleVNCButton(); } if (!Helpers.CreamOrGreater(Source.Connection) || !parentVNCTabView.IsRDPControlEnabled()) { if (InDefaultConsole()) { parentVNCTabView.DisableToggleVNCButton(); } //Start the polling again if (Source != null && !Source.IsControlDomainZero()) { if (!Source.IsHVM()) { connectionPoller = new Timer(PollVNCPort, null, RETRY_SLEEP_TIME, RDP_POLL_INTERVAL); } else if (hasRDP) { connectionPoller = new Timer(PollRDPPort, null, RETRY_SLEEP_TIME, RDP_POLL_INTERVAL); } } } } private void VM_PropertyChanged(object sender, PropertyChangedEventArgs e) { VM vm = (VM)sender; if (vm.uuid != Source.uuid) return; //We only want to reconnect the consoles when they change //if the vm is running and if we are using vncTerm / qemu if (e.PropertyName == "consoles" && vm.power_state == vm_power_state.Running && UseSource) { ConnectNewHostedConsole(); } //If the consoles change under us then refresh hostedConsoles else if (e.PropertyName == "consoles" && vm.power_state == vm_power_state.Running && !UseSource) { lock (hostedConsolesLock) { hostedConsoles = Source.consoles; } } //Or if the VM legitimately turns on else if (e.PropertyName == "power_state" && vm.power_state == vm_power_state.Running) { parentVNCTabView.VMPowerOn(); ConnectNewHostedConsole(); if (connectionPoller != null) connectionPoller.Change(RETRY_SLEEP_TIME, RDP_POLL_INTERVAL); } else if (e.PropertyName == "power_state" && (vm.power_state == vm_power_state.Halted || vm.power_state == vm_power_state.Suspended)) { parentVNCTabView.VMPowerOff(); if (connectionPoller != null) connectionPoller.Change(Timeout.Infinite, Timeout.Infinite); } else if (e.PropertyName == "domid") { // Reboot / start / shutdown Program.Invoke(this, startPolling); } if (e.PropertyName == "power_state" || e.PropertyName == "VGPUs") { Program.Invoke(this, () => { if (GpuStatusChanged != null) GpuStatusChanged(MustConnectRemoteDesktop()); }); } if (e.PropertyName == "name_label" && ConnectionNameChanged != null) ConnectionNameChanged(ConnectionName); } internal void imediatelyPollForConsole() { if (connectionPoller != null) connectionPoller.Change(0, RDP_POLL_INTERVAL); } private void ConnectNewHostedConsole() { Program.AssertOnEventThread(); lock (hostedConsolesLock) { hostedConsoles = Source.consoles; } if (UseVNC && vncClient != null && ConnectionSuperceded()) { initSubControl(); } } /// /// A connection is superceded if it's connected to a console that's no longer being /// advertised by the server and there's a replacement that _is_ being advertised, or /// if its not connected at all. /// /// The server will leave the console open until we close, so that the user may see any crash messages. /// For this reason, we need to close down ourselves when we see that the console has /// been replaced by a newer one (i.e. after a reboot). /// private bool ConnectionSuperceded() { return !vncClient.Connected || ConsoleSuperceded((Console)vncClient.Console); } private bool ConsoleSuperceded(Console old_console) { if (old_console == null) return true; List consoles; lock (hostedConsolesLock) { consoles = Source.Connection.ResolveAll(hostedConsoles); } bool good_console = false; foreach (Console console in consoles) { if (console.opaque_ref == old_console.opaque_ref && console.location == old_console.location) return false; else if (console.protocol == console_protocol.rfb) good_console = true; } return good_console; } /// /// CA-11201: GUI logs are being massively spammed. Prevent "INTERNAL_ERROR Host has disappeared" /// appearing more than once. /// private bool _suppressHostGoneMessage = false; private void Connect(object o) { if (Program.RunInAutomatedTestMode) return; Program.AssertOffEventThread(); KeyValuePair kvp = (KeyValuePair)o; VNCGraphicsClient v = kvp.Key; Exception error = kvp.Value; try { if (UseSource) { List consoles; lock (hostedConsolesLock) { consoles = sourceVM.Connection.ResolveAll(hostedConsoles); } foreach (Console console in consoles) { if (vncClient != v) { // We've been replaced. Give up. return; } if (console.protocol == console_protocol.rfb) { try { ConnectHostedConsole(v, console); return; } catch (Exception exn) { Failure failure = exn as Failure; bool isHostGoneMessage = failure != null && failure.ErrorDescription.Count == 2 && failure.ErrorDescription[0] == Failure.INTERNAL_ERROR && failure.ErrorDescription[1] == Messages.VNC_HOST_GONE; if (isHostGoneMessage) { if (!_suppressHostGoneMessage) { Log.Debug(exn, exn); _suppressHostGoneMessage = true; } } else { Log.Debug(exn, exn); _suppressHostGoneMessage = false; } } } } Log.Debug("Did not find any hosted consoles"); SleepAndRetryConnection(v); } else { if (vncIP == null) { Log.DebugFormat("vncIP is null. Abort VNC connection attempt"); OnVncConnectionAttemptCancelled(); return; } this.vncPassword = Settings.GetVNCPassword(sourceVM.uuid); if (this.vncPassword == null) { bool lifecycleOperationInProgress = sourceVM.current_operations.Values.Any(VM.is_lifecycle_operation); if (haveTriedLoginWithoutPassword && !lifecycleOperationInProgress) { Program.Invoke(this, delegate { promptForPassword(ignoreNextError ? null : error); }); ignoreNextError = false; if (this.vncPassword == null) { Log.Debug("User cancelled VNC password prompt: aborting connection attempt"); OnUserCancelledAuth(); return; } } else { Log.Debug("Attempting passwordless VNC login"); this.vncPassword = new char[0]; ignoreNextError = true; haveTriedLoginWithoutPassword = true; } } Stream s; lock (pendingVNCConnectionLock) { s = pendingVNCConnection; Log.DebugFormat("Using pending VNC connection"); pendingVNCConnection = null; } if (s == null) { Log.DebugFormat("Connecting to vncIP={0}, port={1}", this.vncIP, VNC_PORT); s = connectGuest(this.vncIP, VNC_PORT, sourceVM.Connection); Log.DebugFormat("Connected to vncIP={0}, port={1}", this.vncIP, VNC_PORT); } InvokeConnection(v, s, null); // store the empty vnc password after a successful passwordless login if (haveTriedLoginWithoutPassword && this.vncPassword.Length == 0) Program.Invoke(this, () => Settings.SetVNCPassword(sourceVM.uuid, this.vncPassword)); } } catch (Exception exn) { Log.Warn(exn, exn); SleepAndRetryConnection(v); } } private void promptForPassword(Exception error) { Program.AssertOnEventThread(); // Prompt for password VNCPasswordDialog f = new VNCPasswordDialog(error, sourceVM); try { if (f.ShowDialog(this) == DialogResult.OK) { // Store password for next time this.vncPassword = f.Password; Settings.SetVNCPassword(sourceVM.uuid, this.vncPassword); } else { // User cancelled } } finally { f.Dispose(); } } private void OnUserCancelledAuth() { Program.Invoke(this, delegate { Log.Debug("User cancelled during VNC authentication"); if (UserCancelledAuth != null) UserCancelledAuth(this, null); }); } private void OnVncConnectionAttemptCancelled() { Program.Invoke(this, delegate { Log.Debug("Cancelled VNC connection attempt"); if (VncConnectionAttemptCancelled != null) VncConnectionAttemptCancelled(this, null); }); } private Stream connectGuest(string ip_address, int port, IXenConnection connection) { string uriString = String.Format("http://{0}:{1}/", ip_address, port); Log.DebugFormat("Trying to connect to: {0}", uriString); return HTTP.ConnectStream(new Uri(uriString), XenAdminConfigManager.Provider.GetProxyFromSettings(connection), true, 0); } private void ConnectHostedConsole(VNCGraphicsClient v, Console console) { Program.AssertOffEventThread(); Host host = console.Connection.Resolve(Source.resident_on); if (host == null) { throw new Failure(Failure.INTERNAL_ERROR, Messages.VNC_HOST_GONE); } Uri uri = new Uri(console.location); string sesssionRef; lock (activeSessionLock) { // use the elevated credentials, if provided, for connecting to the console (CA-91132) activeSession = (string.IsNullOrEmpty(ElevatedUsername) || string.IsNullOrEmpty(ElevatedPassword)) ? console.Connection.DuplicateSession() : console.Connection.ElevatedSession(ElevatedUsername, ElevatedPassword); sesssionRef = activeSession.opaque_ref; } Stream stream = HTTPHelper.CONNECT(uri, console.Connection, sesssionRef, false, true); InvokeConnection(v, stream, console); } private void InvokeConnection(VNCGraphicsClient v, Stream stream, Console console) { Program.Invoke(this, delegate() { // This is the last chance that we have to make sure that we've not already // connected this VNCGraphicsClient. Now that we are back on the event thread, // we're guaranteed that no-one will beat us to the v.connect() call. We // hand over responsibility for closing the stream at that point, so we have to // close it ourselves if the client is already connected. if (v.Connected || v.Terminated) { stream.Close(); } else { v.SendScanCodes = UseSource && !this.sourceIsPV; v.SourceVM = sourceVM; v.Console = console; v.UseQemuExtKeyEncoding = sourceVM != null && Helpers.InvernessOrGreater(sourceVM.Connection); v.connect(stream, this.vncPassword); } }); } private void RetryConnection(VNCGraphicsClient v, Exception exn) { if (vncClient == v && !v.Terminated && Source.power_state == vm_power_state.Running) { ThreadPool.QueueUserWorkItem(new WaitCallback(Connect), new KeyValuePair(v, exn)); } } public void SendCAD() { Program.AssertOnEventThread(); if (RemoteConsole != null) { RemoteConsole.SendCAD(); } } private String errorMessage = null; private void ErrorHandler(object sender, Exception exn) { Program.AssertOffEventThread(); if (this.Disposing || this.IsDisposed) return; Program.Invoke(this, delegate() { VNCGraphicsClient v = (VNCGraphicsClient)sender; if (exn is VNCAuthenticationException || exn is CryptographicException) { Log.Debug(exn, exn); // Clear the stored VNC password for this server. Settings.SetVNCPassword(sourceVM.uuid, null); RetryConnection(v, exn); } else if (exn is IOException || exn is Failure) { Log.Debug(exn, exn); SleepAndRetryConnection_(v); } else { Log.Warn(exn, exn); this.errorMessage = exn.Message; } }); } private void SleepAndRetryConnection_(VNCGraphicsClient v) { ThreadPool.QueueUserWorkItem(new WaitCallback(SleepAndRetryConnection), v); } private void SleepAndRetryConnection(object o) { VNCGraphicsClient v = (VNCGraphicsClient)o; Program.AssertOffEventThread(); ConnectionRetries++; Thread.Sleep(ConnectionRetries < SHORT_RETRY_COUNT ? SHORT_RETRY_SLEEP_TIME : RETRY_SLEEP_TIME); RetryConnection(v, null); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (this.errorMessage != null) { SizeF size = e.Graphics.MeasureString(this.errorMessage, this.Font); e.Graphics.DrawString(this.errorMessage, this.Font, Brushes.Black, ((this.Width - size.Width) / 2), ((this.Height - size.Height) / 2)); } // draw focus rectangle if (DisplayFocusRectangle && this.ContainsFocus && RemoteConsole != null) { Rectangle focusRect = Rectangle.Inflate(RemoteConsole.ConsoleBounds, VNCGraphicsClient.BORDER_PADDING / 2, VNCGraphicsClient.BORDER_PADDING / 2); using (Pen pen = new Pen(focusColor, VNCGraphicsClient.BORDER_WIDTH)) { if (this.Focused) pen.DashStyle = DashStyle.Dash; e.Graphics.DrawRectangle(pen, focusRect); } } } // Save this for when we init a new vncClient. private bool displayFocusRectangle = true; public bool DisplayFocusRectangle { get { return displayFocusRectangle; } set { displayFocusRectangle = value; if (RemoteConsole != null) { RemoteConsole.DisplayBorder = displayFocusRectangle; } } } internal Image Snapshot() { if (RemoteConsole != null) return RemoteConsole.Snapshot(); return null; } internal void RefreshScreen() { Program.AssertOnEventThread(); if (RemoteConsole != null && RemoteConsole.ConsoleControl != null) { RemoteConsole.ConsoleControl.Refresh(); } Invalidate(); Update(); } protected override void OnGotFocus(EventArgs e) { Program.AssertOnEventThread(); base.OnGotFocus(e); RefreshScreen(); } protected override void OnLostFocus(EventArgs e) { Program.AssertOnEventThread(); base.OnLostFocus(e); this.pressedKeys = new Set(); // reset tab stop SetKeyboardAndMouseCapture(autoCaptureKeyboardAndMouse); RefreshScreen(); } protected override void OnEnter(EventArgs e) { Program.AssertOnEventThread(); base.OnEnter(e); CaptureKeyboardAndMouse(); RefreshScreen(); } protected override void OnLeave(EventArgs e) { Program.AssertOnEventThread(); base.OnLeave(e); // reset tab stop SetKeyboardAndMouseCapture(autoCaptureKeyboardAndMouse); RefreshScreen(); } internal void UncaptureKeyboardAndMouse() { if (autoCaptureKeyboardAndMouse) { SetKeyboardAndMouseCapture(false); } ActiveControl = null; EnableMenuShortcuts(); } internal void CaptureKeyboardAndMouse() { if (RemoteConsole != null) { RemoteConsole.Activate(); if (autoCaptureKeyboardAndMouse) { SetKeyboardAndMouseCapture(true); } Unpause(); } DisableMenuShortcuts(); } public static void DisableMenuShortcuts() { Program.MainWindow.MenuShortcuts = false; } public static void EnableMenuShortcuts() { Program.MainWindow.MenuShortcuts = true; } private Set pressedKeys = new Set(); private bool modifierKeyPressedAlone = false; protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { const int WM_KEYDOWN = 0x100; const int WM_SYSKEYDOWN = 0x104; bool down = ((msg.Msg == WM_KEYDOWN) || (msg.Msg == WM_SYSKEYDOWN)); Keys key = keyData; if ((key & Keys.Control) == Keys.Control) key = key & ~Keys.Control; if ((key & Keys.Alt) == Keys.Alt) key = key & ~Keys.Alt; if ((key & Keys.Shift) == Keys.Shift) key = key & ~Keys.Shift; // use TranslateKeyMessage to identify if Left or Right modifier keys have been pressed/released Keys extKey = ConsoleKeyHandler.TranslateKeyMessage(msg); return Keysym(down, key, extKey); } protected override void OnKeyUp(KeyEventArgs e) { e.Handled = Keysym(false, e.KeyCode, e.KeyCode); } private bool Keysym(bool pressed, Keys key, Keys extendedKey) { if (!pressed && pressedKeys.Count == 0) // we received key-up, but not key-down - ignore return true; if (KeyHandler.handleExtras(pressed, pressedKeys, KeyHandler.ExtraKeys, extendedKey, KeyHandler.ModifierKeys, ref modifierKeyPressedAlone)) { this.Focus(); return true; } // on keyup, try to remove extended keys (i.e. LControlKey, LControlKey, RShiftKey, LShiftKey, RMenu, LMenu) // we need to do this here, because we cannot otherwise distinguish between Left and Right modifier keys on KeyUp if (!pressed) { List extendedKeys = ConsoleKeyHandler.GetExtendedKeys(key); foreach (var k in extendedKeys) { pressedKeys.Remove(k); } } if (key == Keys.Tab || (key == (Keys.Tab | Keys.Shift))) return false; return false; } private Size oldSize; public void UpdateRDPResolution(bool fullscreen = false) { if (rdpClient == null || oldSize.Equals(this.Size)) return; //no offsets in fullscreen mode because there is no need to accomodate focus border if (fullscreen) rdpClient.UpdateDisplay(this.Size.Width, this.Size.Height, new Point(0,0)); else rdpClient.UpdateDisplay(this.Size.Width - CONSOLE_SIZE_OFFSET, this.Size.Height - CONSOLE_SIZE_OFFSET, new Point(3,3)); oldSize = new Size(this.Size.Width, this.Size.Height); Refresh(); } } }