mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-25 06:16:37 +01:00
42df02734a
Also ensuring that if the UpdateSessionDisplaySettings function (which we use for resize) is not supported, then we display the console centered in the parent window and with scrollbars if needed Signed-off-by: Mihaela Stoica <mihaela.stoica@citrix.com>
1405 lines
50 KiB
C#
1405 lines
50 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 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;
|
|
|
|
/// <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;
|
|
public event Action<bool> GpuStatusChanged;
|
|
public event Action<string> ConnectionNameChanged;
|
|
|
|
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>();
|
|
List<string> ipAddressesForNetworksWithoutPifs = new List<string>();
|
|
List<string> ipv6AddressesForNetworksWithoutPifs = new List<string>();
|
|
|
|
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")) // 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.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;
|
|
}
|
|
|
|
/// <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();
|
|
//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<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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|