/*
Myrtille: A native HTML4/5 Remote Desktop Protocol client.
Copyright(c) 2014-2021 Cedric Coste
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;
using System.Web.SessionState;
using Myrtille.Helpers;
using Myrtille.Services.Contracts;
namespace Myrtille.Web
{
public class RemoteSessionManager : IDisposable
{
#region Init
public RemoteSession RemoteSession { get; private set; }
public RemoteSessionManager(RemoteSession remoteSession)
{
try
{
RemoteSession = remoteSession;
// remote session process client and callback
var callback = new RemoteSessionProcessClientCallback(this, HttpContext.Current.Application);
var callbackContext = new InstanceContext(callback);
HostClient = new RemoteSessionProcessClient(this, callbackContext);
// clients
Clients = new Dictionary();
ClientsLock = new object();
// pipes
Pipes = new RemoteSessionPipes(RemoteSession);
// images event
_imageEventLock = new object();
// cache
_cache = (Cache)HttpContext.Current.Application[HttpApplicationStateVariables.Cache.ToString()];
// owner idle timeout
if (!int.TryParse(ConfigurationManager.AppSettings["OwnerIdleTimeout"], out _ownerIdleTimeoutDelay))
{
_ownerIdleTimeoutDelay = 0;
}
if (_ownerIdleTimeoutDelay > 0)
{
OwnerIdleTimeout = new CancellationTokenSource();
}
// guests idle timeout
_guestsIdleTimeout = new Dictionary();
_guestsIdleTimeoutLock = new object();
// clients idle timeout
_clientsIdleTimeout = new Dictionary();
_clientsIdleTimeoutLock = new object();
}
catch (Exception exc)
{
Trace.TraceError("Failed to initialize remote session manager, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
#endregion
#region Process
public RemoteSessionProcessClient HostClient { get; private set; }
#endregion
#region Pipes
public RemoteSessionPipes Pipes { get; private set; }
public void ProcessUpdatesPipeData(byte[] data)
{
try
{
string message = null;
// image structure: tag (4 bytes) + info (32 bytes) + data
// > tag is used to identify an image (0: image; other: message)
// > info contains the image metadata (idx, posX, posY, etc.)
// > data is the image raw data
var imgTag = BitConverter.ToInt32(data, 0);
if (imgTag != 0)
{
// RDP: UTF16-LE, 2 bytes (16 bits) per character
// SSH: UTF-8, 1 byte (8 bits) per character
message = RemoteSession.HostType == HostType.RDP ? Encoding.Unicode.GetString(data) : Encoding.UTF8.GetString(data);
}
// message
if (!string.IsNullOrEmpty(message))
{
// request page reload
if (message.Equals("reload"))
{
SendMessage(new RemoteSessionMessage { Type = MessageType.PageReload });
}
// remote clipboard
// truncated above max length
else if (message.StartsWith("clipboard|"))
{
RemoteSession.ClipboardText = message.Remove(0, 10);
SendMessage(new RemoteSessionMessage { Type = MessageType.RemoteClipboard, Text = message.Remove(0, 10) });
}
// SSH Terminal data
else if (message.StartsWith("term|"))
{
if (RemoteSession.State == RemoteSessionState.Connecting)
{
RemoteSession.State = RemoteSessionState.Connected;
SendMessage(new RemoteSessionMessage { Type = MessageType.Connected });
}
SendMessage(new RemoteSessionMessage { Type = MessageType.TerminalOutput, Text = message.Remove(0, 5) });
}
// print job
else if (message.StartsWith("printjob|"))
{
SendMessage(new RemoteSessionMessage { Type = MessageType.PrintJob, Text = message.Remove(0, 9) });
}
}
// image
else
{
if (RemoteSession.State == RemoteSessionState.Connecting)
{
RemoteSession.State = RemoteSessionState.Connected;
SendMessage(new RemoteSessionMessage { Type = MessageType.Connected });
// in case the remote session was reconnected, send the capture API config
SendCommand(RemoteSessionCommand.SetScreenshotConfig, string.Format("{0}|{1}|{2}", RemoteSession.ScreenshotIntervalSecs, (int)RemoteSession.ScreenshotFormat, RemoteSession.ScreenshotPath));
}
ProcessUpdate(data);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to process updates pipe message, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
public void ProcessAudioPipeData(byte[] data)
{
try
{
if (RemoteSession.State == RemoteSessionState.Connected)
{
ProcessAudio(data);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to process audio pipe message, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
#endregion
#region Commands
public void SendCommand(RemoteSessionCommand command, string args = "")
{
try
{
if (RemoteSession.State == RemoteSessionState.NotConnected ||
RemoteSession.State == RemoteSessionState.Disconnected)
return;
var commandWithArgs = string.Concat((string)RemoteSessionCommandMapping.ToPrefix[command], args);
switch (command)
{
// as the process command line can be displayed into the task manager / process explorer, the connection settings (including user credentials) are now passed to the host client through the inputs pipe
// their values are set from the login page (using http(s) get or post) and shouldn't be modified at this step
case RemoteSessionCommand.SendServerAddress:
case RemoteSessionCommand.SendVMGuid:
case RemoteSessionCommand.SendUserDomain:
case RemoteSessionCommand.SendUserName:
case RemoteSessionCommand.SendUserPassword:
case RemoteSessionCommand.SendStartProgram:
if (RemoteSession.State != RemoteSessionState.Connecting)
return;
break;
// browser resize
case RemoteSessionCommand.SendBrowserResize:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
if (RemoteSession.BrowserResize == BrowserResize.None)
return;
if (_resizeDelayed)
{
if (_resizeTimeout != null)
{
_resizeTimeout.Cancel();
_resizeTimeout.Dispose();
}
_resizeTimeout = new CancellationTokenSource();
Task.Delay(500, _resizeTimeout.Token).ContinueWith(task =>
{
var parts = args.Split(new[] { "|" }, StringSplitOptions.None);
var resolution = parts[1].Split(new[] { "x" }, StringSplitOptions.None);
var width = int.Parse(resolution[0]);
var height = int.Parse(resolution[1]);
RemoteSession.ClientWidth = width < 100 ? 100 : width;
RemoteSession.ClientHeight = height < 100 ? 100 : height;
if (RemoteSession.BrowserResize == BrowserResize.Reconnect)
{
RemoteSession.Reconnect = true;
SendCommand(RemoteSessionCommand.CloseClient);
}
else if (RemoteSession.BrowserResize == BrowserResize.Scale)
{
_resizeDelayed = false;
SendCommand(RemoteSessionCommand.SendBrowserResize, args);
}
}, TaskContinuationOptions.NotOnCanceled);
return;
}
_resizeDelayed = true;
break;
// browser pulse
case RemoteSessionCommand.SendBrowserPulse:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
break;
// keyboard, mouse
case RemoteSessionCommand.SendKeyUnicode:
case RemoteSessionCommand.SendMouseMove:
case RemoteSessionCommand.SendMouseLeftButton:
case RemoteSessionCommand.SendMouseMiddleButton:
case RemoteSessionCommand.SendMouseRightButton:
case RemoteSessionCommand.SendMouseWheelUp:
case RemoteSessionCommand.SendMouseWheelDown:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
break;
case RemoteSessionCommand.SendKeyScancode:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
var keyCodeAndState = args.Split(new[] { "-" }, StringSplitOptions.None);
var jsKeyCode = int.Parse(keyCodeAndState[0]);
var keyState = keyCodeAndState[1];
var rdpScanCode = JsKeyCodeToRdpScanCodeMapping.MapTable[jsKeyCode] as RdpScanCode;
if (rdpScanCode != null && rdpScanCode.Value != 0)
{
commandWithArgs = string.Concat((string)RemoteSessionCommandMapping.ToPrefix[command], rdpScanCode.Value + "-" + keyState + "-" + (rdpScanCode.Extend ? "1" : "0"));
}
else
{
return;
}
break;
// control
case RemoteSessionCommand.SetScaleDisplay:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Display scaling {0}, remote session {1}", args != "0" ? args : "OFF", RemoteSession.Id);
RemoteSession.BrowserResize = args != "0" ? BrowserResize.Scale : BrowserResize.None;
break;
case RemoteSessionCommand.SetReconnectSession:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Session reconnect {0}, remote session {1}", args.StartsWith("1") ? "ON" : "OFF", RemoteSession.Id);
RemoteSession.BrowserResize = args.StartsWith("1") ? BrowserResize.Reconnect : BrowserResize.None;
break;
case RemoteSessionCommand.SetImageEncoding:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Image encoding {0}, remote session {1}", int.Parse(args), RemoteSession.Id);
RemoteSession.ImageEncoding = (ImageEncoding)int.Parse(args);
break;
case RemoteSessionCommand.SetImageQuality:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Image quality {0}, remote session {1}", int.Parse(args), RemoteSession.Id);
RemoteSession.ImageQuality = int.Parse(args);
break;
case RemoteSessionCommand.SetImageQuantity:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Image quantity {0}, remote session {1}", int.Parse(args), RemoteSession.Id);
RemoteSession.ImageQuantity = int.Parse(args);
break;
case RemoteSessionCommand.SetAudioFormat:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Audio format {0}, remote session {1}", int.Parse(args), RemoteSession.Id);
RemoteSession.AudioFormat = (AudioFormat)int.Parse(args);
break;
case RemoteSessionCommand.SetAudioBitrate:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
Trace.TraceInformation("Audio bitrate {0}, remote session {1}", int.Parse(args), RemoteSession.Id);
RemoteSession.AudioBitrate = int.Parse(args);
break;
case RemoteSessionCommand.SetScreenshotConfig:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
var config = args.Split(new[] { "|" }, StringSplitOptions.None);
RemoteSession.ScreenshotIntervalSecs = int.Parse(config[0]);
RemoteSession.ScreenshotFormat = (CaptureFormat)int.Parse(config[1]);
RemoteSession.ScreenshotPath = config[2];
Trace.TraceInformation("Screenshot config {0}, remote session {1}", args, RemoteSession.Id);
break;
case RemoteSessionCommand.StartTakingScreenshots:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
if (_screenshotTimeout != null)
{
_screenshotTimeout.Cancel();
_screenshotTimeout.Dispose();
}
_screenshotTimeout = new CancellationTokenSource();
SendCommand(RemoteSessionCommand.TakeScreenshot);
Trace.TraceInformation("Starting taking screenshots {0}, remote session {1}", args, RemoteSession.Id);
break;
case RemoteSessionCommand.StopTakingScreenshots:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
if (_screenshotTimeout != null)
{
_screenshotTimeout.Cancel();
_screenshotTimeout.Dispose();
}
_screenshotTimeout = null;
Trace.TraceInformation("Stopping taking screenshots {0}, remote session {1}", args, RemoteSession.Id);
break;
case RemoteSessionCommand.TakeScreenshot:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
if (_screenshotTimeout != null)
{
_screenshotTimeout.Cancel();
_screenshotTimeout.Dispose();
_screenshotTimeout = new CancellationTokenSource();
Task.Delay(RemoteSession.ScreenshotIntervalSecs * 1000, _screenshotTimeout.Token).ContinueWith(task =>
{
SendCommand(RemoteSessionCommand.TakeScreenshot);
}, TaskContinuationOptions.NotOnCanceled);
}
FullscreenEventPending = true;
Trace.TraceInformation("Taking screenshot {0}, remote session {1}", args, RemoteSession.Id);
break;
case RemoteSessionCommand.RequestFullscreenUpdate:
if (RemoteSession.State != RemoteSessionState.Connected)
return;
FullscreenEventPending = true;
Trace.TraceInformation("Requesting fullscreen update, all image(s) will now be discarded while waiting for it, remote session {0}", RemoteSession.Id);
break;
case RemoteSessionCommand.SendLocalClipboard:
if ((RemoteSession.State != RemoteSessionState.Connecting) &&
(RemoteSession.State != RemoteSessionState.Connected))
return;
var clipboardText = string.Empty;
// read the clipboard text from unicode code points
var charsCodes = args.Split(new[] { "-" }, StringSplitOptions.None);
foreach (var charCode in charsCodes)
{
clipboardText += char.ConvertFromUtf32(int.Parse(charCode));
}
// truncated above max length, which was normally already enforced client side; re-checking
if (clipboardText.Length > _clipboardMaxLength)
{
clipboardText = clipboardText.Substring(0, _clipboardMaxLength) + "--- TRUNCATED ---";
}
// set the clipboard text on the gateway; if the remote session is reconnected, it will be sent to the new instance of wfreerdp
RemoteSession.ClipboardText = clipboardText;
commandWithArgs = string.Concat((string)RemoteSessionCommandMapping.ToPrefix[command], clipboardText);
Trace.TraceInformation("Sending local clipboard, remote session {0}", RemoteSession.Id);
break;
case RemoteSessionCommand.ConnectClient:
if (RemoteSession.State != RemoteSessionState.Connecting)
return;
Trace.TraceInformation("Connecting remote session, remote session {0}", RemoteSession.Id);
break;
case RemoteSessionCommand.CloseClient:
if ((RemoteSession.State != RemoteSessionState.Connecting) &&
(RemoteSession.State != RemoteSessionState.Connected))
return;
RemoteSession.State = RemoteSessionState.Disconnecting;
Trace.TraceInformation("disconnecting remote session, remote session {0}", RemoteSession.Id);
break;
}
Trace.TraceInformation("Sending command with args {0}, remote session {1}", commandWithArgs, RemoteSession.Id);
PipeHelper.WritePipeData(
Pipes.InputsPipe,
"remotesession_" + RemoteSession.Id + "_inputs",
commandWithArgs);
}
catch (Exception exc)
{
Trace.TraceWarning("Failed to send command {0}, args {1}, remote session {2} ({3})", command, args, RemoteSession?.Id, exc);
// there is a problem with the inputs pipe, force close the remote session in order to avoid it being stuck
// it's usually not a big deal, some inputs being sent while the pipes are being disconnected when the host client is closed (and thus before the remote session state is set to disconnected), but better take no risk...
HostClient.StopProcess();
}
}
#endregion
#region Inputs
public void ProcessInputs(HttpSessionState session, string clientId, string data)
{
if (RemoteSession.State == RemoteSessionState.NotConnected ||
RemoteSession.State == RemoteSessionState.Disconnected)
return;
try
{
// monitor the activity of the remote session owner; if its browser window/tab is closed without disconnecting first, or if the connection is lost, there won't be anymore input
// in that case, disconnect the remote session after some time (set in web.config); if the session is shared, guests will be disconnected too
// this comes in addition (but not replace) the session idle timeout which may defined (or not) for the remote server
if (session.SessionID.Equals(RemoteSession.OwnerSessionID))
{
if (OwnerIdleTimeout != null)
{
OwnerIdleTimeout.Cancel();
OwnerIdleTimeout.Dispose();
OwnerIdleTimeout = new CancellationTokenSource();
Task.Delay(_ownerIdleTimeoutDelay, OwnerIdleTimeout.Token).ContinueWith(task =>
{
if (RemoteSession.State == RemoteSessionState.Connecting ||
RemoteSession.State == RemoteSessionState.Connected)
{
SendCommand(RemoteSessionCommand.CloseClient);
}
}, TaskContinuationOptions.NotOnCanceled);
}
}
// same thing for guests, except that it doesn't disconnect the remote session
// instead, it just frees a slot so that another invited guest can connect it
else
{
lock (_guestsIdleTimeoutLock)
{
if (!_guestsIdleTimeout.ContainsKey(session.SessionID))
{
_guestsIdleTimeout.Add(session.SessionID, new CancellationTokenSource());
}
var guestIdleTimeout = _guestsIdleTimeout[session.SessionID];
if (guestIdleTimeout != null)
{
guestIdleTimeout.Cancel();
guestIdleTimeout.Dispose();
}
guestIdleTimeout = new CancellationTokenSource();
// guest idle time is fixed at 1mn; because of the periodical FSUs (even for guests with no control) this can only happen if the guest closes its browser window/tab or if the connection is lost
Task.Delay(60000, guestIdleTimeout.Token).ContinueWith(task =>
{
if (RemoteSession.State != RemoteSessionState.Connected)
return;
session[HttpSessionStateVariables.RemoteSession.ToString()] = null;
if (session[HttpSessionStateVariables.GuestInfo.ToString()] != null)
{
// an inactive guest isn't removed, it just looses its slot; if there is an available slot afterward, the guest can reclaim it
((GuestInfo)session[HttpSessionStateVariables.GuestInfo.ToString()]).Active = false;
if (RemoteSession.ActiveGuests > 0)
{
RemoteSession.ActiveGuests--;
}
}
if (_guestsIdleTimeout.ContainsKey(session.SessionID))
{
_guestsIdleTimeout.Remove(session.SessionID);
}
}, TaskContinuationOptions.NotOnCanceled);
_guestsIdleTimeout[session.SessionID] = guestIdleTimeout;
}
}
// monitor the activity of the remote session client (browser window/tab),
// regardless of whether it's the remote session owner or a guest, whose URL is shared or not
// idle time is fixed at 1mn
if (Clients.ContainsKey(clientId))
{
lock (_clientsIdleTimeoutLock)
{
if (!_clientsIdleTimeout.ContainsKey(clientId))
{
_clientsIdleTimeout.Add(clientId, new CancellationTokenSource());
}
var clientIdleTimeout = _clientsIdleTimeout[clientId];
if (clientIdleTimeout != null)
{
clientIdleTimeout.Cancel();
clientIdleTimeout.Dispose();
}
clientIdleTimeout = new CancellationTokenSource();
Task.Delay(60000, clientIdleTimeout.Token).ContinueWith(task =>
{
if (Clients.ContainsKey(clientId))
{
lock (ClientsLock)
{
Clients.Remove(clientId);
}
}
}, TaskContinuationOptions.NotOnCanceled);
_clientsIdleTimeout[clientId] = clientIdleTimeout;
}
}
var inputs = data.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
foreach (var input in inputs)
{
if (!string.IsNullOrEmpty(input))
{
var command = (RemoteSessionCommand)RemoteSessionCommandMapping.FromPrefix[input.Substring(0, 3)];
// if the remote session is shared, only the remote session owner and guests with control access can interact with it
// for the latter, such a control is limited to some actions (not sharing it with others persons or change the remote session configuration)
// only FSUs is allowed for anyone to update their display
// TODO: maintain a list of guests for the remote session, with different settings for each guest, then have different processings accordingly
if (session.SessionID.Equals(RemoteSession.OwnerSessionID) ||
(session[HttpSessionStateVariables.GuestInfo.ToString()] != null &&
((GuestInfo)session[HttpSessionStateVariables.GuestInfo.ToString()]).Control &&
(command == RemoteSessionCommand.SendKeyUnicode ||
command == RemoteSessionCommand.SendKeyScancode ||
command == RemoteSessionCommand.SendMouseLeftButton ||
command == RemoteSessionCommand.SendMouseMiddleButton ||
command == RemoteSessionCommand.SendMouseRightButton ||
command == RemoteSessionCommand.SendMouseWheelUp ||
command == RemoteSessionCommand.SendMouseWheelDown ||
command == RemoteSessionCommand.SendMouseMove ||
command == RemoteSessionCommand.SendLocalClipboard)) ||
command == RemoteSessionCommand.RequestFullscreenUpdate)
{
SendCommand(command, input.Remove(0, 3));
}
}
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to process input(s) {0}, remote session {1} ({2})", data, RemoteSession?.Id, exc);
}
}
#endregion
#region Owner
private bool _resizeDelayed = true;
private CancellationTokenSource _resizeTimeout;
private int _ownerIdleTimeoutDelay = 0;
public CancellationTokenSource OwnerIdleTimeout { get; set; }
#endregion
#region Guests
// TODO: better guests management from here
private IDictionary _guestsIdleTimeout;
private object _guestsIdleTimeoutLock;
#endregion
#region Clients
// a remote session client is a browser window/tab,
// regardless of whether it's the remote session owner or a guest, whose URL is shared or not
// it's given an unique id on each page load
public IDictionary Clients { get; private set; }
public object ClientsLock { get; private set; }
private IDictionary _clientsIdleTimeout;
private object _clientsIdleTimeoutLock;
#endregion
#region Updates
// new image
private void ProcessUpdate(byte[] data)
{
try
{
if (data.Length <= 36)
throw new Exception("invalid image data");
// image info (8 items, 32 bits each => 8 * 4 = 32 bytes)
var imgInfo = new byte[32];
Array.Copy(data, 4, imgInfo, 0, 32);
// CAUTION! if the remote session is reconnected (i.e.: browser resize), a new instance of wfreerdp is spawned
// the image index can no longer be handled by wfreerdp, this must be done by the remote session manager
// the image index provided by wfreerdp is relative to its own instance, if needed
var image = new RemoteSessionImage
{
//Idx = BitConverter.ToInt32(imgInfo, 0),
Idx = _imageIdx == int.MaxValue ? 1 : ++_imageIdx,
PosX = BitConverter.ToInt32(imgInfo, 4),
PosY = BitConverter.ToInt32(imgInfo, 8),
Width = BitConverter.ToInt32(imgInfo, 12),
Height = BitConverter.ToInt32(imgInfo, 16),
Format = (ImageFormat)BitConverter.ToInt32(imgInfo, 20),
Quality = BitConverter.ToInt32(imgInfo, 24),
Fullscreen = BitConverter.ToInt32(imgInfo, 28) == 1,
Data = new byte[data.Length - 36]
};
Array.Copy(data, 36, image.Data, 0, data.Length - 36);
// cache the image, even if using websocket (used to retrieve the mouse cursor on IE)
_cache.Insert(
"remoteSessionImage_" + RemoteSession.Id + "_" + image.Idx,
image,
null,
DateTime.Now.AddMilliseconds(_imageCacheDuration),
Cache.NoSlidingExpiration);
Trace.TraceInformation("Received image {0} ({1}), remote session {2}", image.Idx, (image.Fullscreen ? "screen" : "region"), RemoteSession.Id);
// a fullscreen update was requested
if (FullscreenEventPending && image.Fullscreen)
{
Trace.TraceInformation("Fullscreen update received, remote session {0}", RemoteSession.Id);
FullscreenEventPending = false;
// screenshot request
if (ScreenshotEventLock != null)
{
lock (ScreenshotEventLock)
{
// screenshot image index
ScreenshotImageIdx = image.Idx;
// if waiting for a screenshot, signal the reception
if (ScreenshotEventPending)
{
ScreenshotEventPending = false;
Monitor.Pulse(ScreenshotEventLock);
}
}
}
}
// send update to client(s)
foreach (var client in Clients.Values)
{
// websocket(s)
if (client.WebSockets != null && client.WebSockets.Count > 0)
{
// round robin on the client websockets down pool
if (client.WebSocketsRoundRobinIdx >= client.WebSockets.Count)
{
client.WebSocketsRoundRobinIdx = 0;
}
//Trace.TraceInformation("Sending image {0} ({1}) on websocket, client {2}, remote session {3}", image.Idx, (image.Fullscreen ? "screen" : "region"), client.Id, RemoteSession.Id);
client.WebSockets[client.WebSocketsRoundRobinIdx++].SendImage(image);
}
// event source
else if (client.EventSource != null)
{
//Trace.TraceInformation("Sending image {0} ({1}) on event source, client {2}, remote session {3}", image.Idx, (image.Fullscreen ? "screen" : "region"), client.Id, RemoteSession.Id);
client.EventSource.SendImage(image);
}
// long polling
else if (client.LongPolling != null)
{
//Trace.TraceInformation("Sending image {0} ({1}) on long polling, client {2}, remote session {3}", image.Idx, (image.Fullscreen ? "screen" : "region"), client.Id, RemoteSession.Id);
client.LongPolling.SendImage(image);
}
// xhr: updates are polled against the cache by the client
}
// image event
lock (_imageEventLock)
{
// last received image index
_lastReceivedImageIdx = image.Idx;
// if waiting for a new image, signal the reception
if (_imageEventPending)
{
_imageEventPending = false;
Monitor.Pulse(_imageEventLock);
}
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to process update, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
// retrieve a cached image
public RemoteSessionImage GetCachedUpdate(int imageIdx)
{
RemoteSessionImage image = null;
try
{
var imageObj = _cache["remoteSessionImage_" + RemoteSession.Id + "_" + imageIdx];
if (imageObj != null)
{
image = (RemoteSessionImage)imageObj;
Trace.TraceInformation("Retrieved image {0} ({1}) from cache, remote session {2}", imageIdx, (image.Fullscreen ? "screen" : "region"), RemoteSession.Id);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to retrieve image {0} from cache, remote session {1} ({2})", imageIdx, RemoteSession?.Id, exc);
}
return image;
}
// retrieve the next image
public RemoteSessionImage GetNextUpdate(int imageIdx, int? waitDuration = null)
{
RemoteSessionImage image = null;
lock (_imageEventLock)
{
try
{
// retrieve the next available image from cache, up to the latest received
if (imageIdx < _lastReceivedImageIdx)
{
for (var idx = imageIdx + 1; idx <= _lastReceivedImageIdx; idx++)
{
image = GetCachedUpdate(idx);
if (image != null)
{
break;
}
}
}
// if no image is available and a wait duration is specified, wait for a new image
if (image == null && waitDuration.HasValue)
{
Trace.TraceInformation("Waiting for new image, remote session {0}", RemoteSession.Id);
_imageEventPending = true;
if (waitDuration.Value > 0)
{
// wait for the specified time
if (Monitor.Wait(_imageEventLock, waitDuration.Value))
{
image = GetCachedUpdate(_lastReceivedImageIdx);
}
}
else
{
// wait indefinitely
if (Monitor.Wait(_imageEventLock))
{
image = GetCachedUpdate(_lastReceivedImageIdx);
}
}
_imageEventPending = false;
Monitor.Pulse(_imageEventLock);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to retrieve next update from index {0}, remote session {1} ({2})", imageIdx, RemoteSession?.Id, exc);
}
}
return image;
}
// retrieve the last image
public RemoteSessionImage GetLastUpdate()
{
return GetCachedUpdate(_lastReceivedImageIdx);
}
#endregion
#region Messages
public void SendMessage(RemoteSessionMessage message)
{
// send message to client(s)
foreach (var client in Clients.Values)
{
// websocket(s)
if (client.WebSockets != null && client.WebSockets.Count > 0)
{
// no round robin there; send message on the first websocket down
Trace.TraceInformation("Sending {0} notification on websocket, client {1}, remote session {2}", message.Type, client.Id, RemoteSession.Id);
client.WebSockets[0].SendMessage(message);
}
// event source
else if (client.EventSource != null)
{
Trace.TraceInformation("Sending {0} notification on event source, client {1}, remote session {2}", message.Type, client.Id, RemoteSession.Id);
client.EventSource.SendMessage(message);
}
// long polling
else if (client.LongPolling != null)
{
Trace.TraceInformation("Sending {0} notification on long polling, client {1}, remote session {2}", message.Type, client.Id, RemoteSession.Id);
client.LongPolling.SendMessage(message);
}
// xhr
else if (client.MessageQueue != null)
{
Trace.TraceInformation("Sending {0} notification on xhr, client {1}, remote session {2}", message.Type, client.Id, RemoteSession.Id);
lock (((ICollection)client.MessageQueue).SyncRoot)
{
client.MessageQueue.Add(message);
}
}
}
StopWaitForImageEvent();
}
#endregion
#region Images
// image index
private int _imageIdx = 0;
// pending fullscreen update
public bool FullscreenEventPending { get; private set; }
// image reception (fullscreen and region)
private object _imageEventLock;
private bool _imageEventPending;
// screenshot
public object ScreenshotEventLock { get; set; }
public bool ScreenshotEventPending { get; set; }
public int ScreenshotImageIdx { get; private set; }
// stop waiting for an image reception (a page reload is requested, the remote session is disconnected, etc.)
public void StopWaitForImageEvent()
{
lock (_imageEventLock)
{
if (_imageEventPending)
{
_imageEventPending = false;
Monitor.Pulse(_imageEventLock);
}
}
}
// last received image
private int _lastReceivedImageIdx = 0;
#endregion
#region Audio
// audio index
private int _audioIdx = 0;
// new audio
private void ProcessAudio(byte[] data)
{
try
{
var audio = new RemoteSessionAudio
{
Idx = _audioIdx == int.MaxValue ? 1 : ++_audioIdx,
Format = RemoteSession.AudioFormat.HasValue ? RemoteSession.AudioFormat.Value : AudioFormat.MP3,
Bitrate = RemoteSession.AudioBitrate.HasValue ? RemoteSession.AudioBitrate.Value : 128,
Data = data
};
_cache.Insert(
"remoteSessionAudio_" + RemoteSession.Id + "_" + audio.Idx,
audio,
null,
DateTime.Now.AddMilliseconds(_audioCacheDuration),
Cache.NoSlidingExpiration);
Trace.TraceInformation("Received audio {0}, remote session {1}", audio.Idx, RemoteSession.Id);
// send audio to client(s)
foreach (var client in Clients.Values)
{
// audio websocket
if (client.AudioWebSocket != null)
{
Trace.TraceInformation("Sending audio {0} on audio websocket, client {1}, remote session {2}", audio.Idx, client.Id, RemoteSession.Id);
client.AudioWebSocket.ProcessAudio(audio);
}
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to process audio, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
// retrieve a cached audio
public RemoteSessionAudio GetCachedAudio(int audioIdx)
{
RemoteSessionAudio audio = null;
try
{
var audioObj = _cache["remoteSessionAudio_" + RemoteSession.Id + "_" + audioIdx];
if (audioObj != null)
{
audio = (RemoteSessionAudio)audioObj;
Trace.TraceInformation("Retrieved audio {0} from cache, remote session {1}", audioIdx, RemoteSession.Id);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to retrieve audio {0} from cache, remote session {1} ({2})", audioIdx, RemoteSession?.Id, exc);
}
return audio;
}
#endregion
#region Capture
private CancellationTokenSource _screenshotTimeout;
#endregion
#region Clipboard
// the clipboard must be limited in size or otherwise create too much network traffic and slowness; 1MB is usually enough for most copy/paste actions
private const int _clipboardMaxLength = 1048576;
#endregion
#region Cache
// when using polling (long-polling or xhr only), images or audio must be cached for a delayed retrieval; not applicable for websocket
private Cache _cache;
// cache lifetime (ms); that is, represents the maximal lag possible for a client, before having to drop some images or audio in order to catch up with the remote session (proceed with caution with these values!)
private const int _imageCacheDuration = 1000;
private const int _audioCacheDuration = 2000;
#endregion
#region IDisposable
~RemoteSessionManager()
{
Dispose();
}
public void Dispose()
{
GC.SuppressFinalize(this);
if (Pipes != null)
{
Pipes.DeletePipes();
}
foreach (var client in Clients.Values)
{
foreach (var webSocket in client.WebSockets)
{
webSocket.Close();
}
if (client.AudioWebSocket != null)
{
client.AudioWebSocket.Close();
}
}
}
#endregion
}
}