mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-12-29 23:30:10 +01:00
1345 lines
48 KiB
C#
1345 lines
48 KiB
C#
/* 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 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 static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
private int ConnectionRetries = 0;
|
|
|
|
private volatile bool useVNC = true;
|
|
|
|
private bool autoCaptureKeyboardAndMouse = true;
|
|
internal bool showConnectionBar = true;
|
|
|
|
private readonly Color focusColor = SystemColors.MenuHighlight;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<XenRef<Console>> hostedConsoles = null;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private Session activeSession = null;
|
|
private readonly Object activeSessionLock = new Object();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private Stream pendingVNCConnection = null;
|
|
private readonly Object pendingVNCConnectionLock = new Object();
|
|
|
|
internal EventHandler ResizeHandler;
|
|
|
|
public event EventHandler UserCancelledAuth;
|
|
public event EventHandler VncConnectionAttemptCancelled;
|
|
|
|
internal readonly VNCTabView parentVNCTabView;
|
|
|
|
[DefaultValue(false)]
|
|
public bool UserWantsToSwitchProtocol { get; set; }
|
|
|
|
private bool hasRDP { get { return Source != null ? Source.HasRDP : false; } }
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private bool haveTriedLoginWithoutPassword = false;
|
|
private bool ignoreNextError = false;
|
|
|
|
private Dictionary<string, string> cachedNetworks;
|
|
|
|
/// <summary>
|
|
/// The last known VNC password for this VM.
|
|
/// </summary>
|
|
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<VM_guest_metrics>(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<VM_guest_metrics>(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<string, string> newNetworks = (sender as VM_guest_metrics).networks;
|
|
if (!equateDictionary<string, string>(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<T, S>(Dictionary<T, S> d1, Dictionary<T, S> d2) where S : IEquatable<S>
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Nothrow guarantee.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// scan each ip address (from the guest agent) for an open port
|
|
/// </summary>
|
|
/// <param name="port"></param>
|
|
public String PollPort(int port, bool vnc)
|
|
{
|
|
try
|
|
{
|
|
if (Source == null)
|
|
return null;
|
|
|
|
VM vm = Source;
|
|
|
|
XenRef<VM_guest_metrics> 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<string, string> networks = metrics.networks;
|
|
|
|
if (networks == null)
|
|
return null;
|
|
|
|
List<string> ipAddresses = new List<string>();
|
|
List<string> ipv6Addresses = new List<string>();
|
|
|
|
foreach (VIF vif in vm.Connection.ResolveAll(vm.VIFs))
|
|
{
|
|
XenAPI.Network network = vif.Connection.Resolve(vif.network);
|
|
XenAPI.Host host = vm.Connection.Resolve(vm.resident_on);
|
|
XenAPI.PIF pif = Helpers.FindPIF(network, host);
|
|
if (pif != null && pif.LinkStatus == PIF.LinkState.Connected)
|
|
{
|
|
foreach (var networkInfo in networks.Where(n => n.Key.StartsWith(String.Format("{0}/ip", vif.device))))
|
|
{
|
|
if (networkInfo.Key.EndsWith("ip")) // IPv4 address
|
|
ipAddresses.Add(networkInfo.Value);
|
|
else
|
|
{
|
|
if (networkInfo.Key.Contains("ipv6")) // IPv6 address, enclose in square brackets
|
|
ipv6Addresses.Add(String.Format("[{0}]", networkInfo.Value));
|
|
else
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ipAddresses.AddRange(ipv6Addresses); // make sure IPv4 addresses are scanned first (CA-102755)
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exchange the current value of pendingVNCConnection with the given one, and close the old
|
|
/// connection, if any.
|
|
/// Nothrow guarantee.
|
|
/// </summary>
|
|
/// <param name="s">May be null</param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the actual VNC or RDP client control.
|
|
/// </summary>
|
|
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();
|
|
|
|
Size oldSize = new Size(1024, 768);
|
|
|
|
// Kill the old client.
|
|
if (RemoteConsole != null)
|
|
{
|
|
oldSize = RemoteConsole.DesktopSize;
|
|
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)
|
|
oldSize = ((FullScreenForm)ParentForm).contentPanel.Size;
|
|
this.AutoScroll = true;
|
|
this.AutoScrollMinSize = oldSize;
|
|
|
|
rdpClient = new RdpClient(this, oldSize, 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();
|
|
}
|
|
|
|
parentVNCTabView.ShowGpuWarningIfRequired();
|
|
}
|
|
|
|
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<VNCGraphicsClient, Exception>(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;
|
|
/// <summary>
|
|
/// Indicates whether to use the source or the detected vncIP
|
|
/// </summary>
|
|
public bool UseSource
|
|
{
|
|
get
|
|
{
|
|
return useSource;
|
|
}
|
|
set
|
|
{
|
|
if (value != useSource)
|
|
{
|
|
useSource = value;
|
|
ConnectionRetries = 0;
|
|
initSubControl();
|
|
}
|
|
}
|
|
}
|
|
|
|
public 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"));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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")
|
|
parentVNCTabView.ShowGpuWarningIfRequired();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
private bool ConnectionSuperceded()
|
|
{
|
|
return !vncClient.Connected || ConsoleSuperceded((Console)vncClient.Console);
|
|
}
|
|
|
|
private bool ConsoleSuperceded(Console old_console)
|
|
{
|
|
if (old_console == null)
|
|
return true;
|
|
|
|
List<Console> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CA-11201: GUI logs are being massively spammed. Prevent "INTERNAL_ERROR Host has disappeared"
|
|
/// appearing more than once.
|
|
/// </summary>
|
|
private bool _suppressHostGoneMessage = false;
|
|
private void Connect(object o)
|
|
{
|
|
if (Program.RunInAutomatedTestMode)
|
|
return;
|
|
|
|
Program.AssertOffEventThread();
|
|
|
|
KeyValuePair<VNCGraphicsClient, Exception> kvp = (KeyValuePair<VNCGraphicsClient, Exception>)o;
|
|
|
|
VNCGraphicsClient v = kvp.Key;
|
|
Exception error = kvp.Value;
|
|
|
|
try
|
|
{
|
|
if (UseSource)
|
|
{
|
|
List<Console> 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 SessionUUID;
|
|
|
|
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);
|
|
SessionUUID = activeSession.uuid;
|
|
}
|
|
|
|
Stream stream = HTTPHelper.CONNECT(uri, console.Connection, SessionUUID, 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.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<VNCGraphicsClient, Exception>(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<Keys>();
|
|
|
|
// 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<Keys> pressedKeys = new Set<Keys>();
|
|
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<Keys>(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<Keys> 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;
|
|
}
|
|
}
|
|
}
|