From 767cdc1f6f6a63ce997fc9476911e2c361f9d402 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Wed, 20 Feb 2013 20:33:05 -0500 Subject: Pushing missing changes --- MediaBrowser.Common/Kernel/TcpManager.cs | 485 +++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 MediaBrowser.Common/Kernel/TcpManager.cs (limited to 'MediaBrowser.Common/Kernel/TcpManager.cs') diff --git a/MediaBrowser.Common/Kernel/TcpManager.cs b/MediaBrowser.Common/Kernel/TcpManager.cs new file mode 100644 index 000000000..02b078c79 --- /dev/null +++ b/MediaBrowser.Common/Kernel/TcpManager.cs @@ -0,0 +1,485 @@ +using Alchemy; +using Alchemy.Classes; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Serialization; +using MediaBrowser.Model.Configuration; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Kernel +{ + /// + /// Manages the Http Server, Udp Server and WebSocket connections + /// + public class TcpManager : BaseManager + { + /// + /// This is the udp server used for server discovery by clients + /// + /// The UDP server. + private UdpServer UdpServer { get; set; } + + /// + /// Gets or sets the UDP listener. + /// + /// The UDP listener. + private IDisposable UdpListener { get; set; } + + /// + /// Both the Ui and server will have a built-in HttpServer. + /// People will inevitably want remote control apps so it's needed in the Ui too. + /// + /// The HTTP server. + public HttpServer HttpServer { get; private set; } + + /// + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// + /// The HTTP listener. + private IDisposable HttpListener { get; set; } + + /// + /// The web socket connections + /// + private readonly List _webSocketConnections = new List(); + + /// + /// Gets or sets the external web socket server. + /// + /// The external web socket server. + private WebSocketServer ExternalWebSocketServer { get; set; } + + /// + /// The _supports native web socket + /// + private bool? _supportsNativeWebSocket; + + /// + /// Gets a value indicating whether [supports web socket]. + /// + /// true if [supports web socket]; otherwise, false. + internal bool SupportsNativeWebSocket + { + get + { + if (!_supportsNativeWebSocket.HasValue) + { + try + { + new ClientWebSocket(); + + _supportsNativeWebSocket = true; + } + catch (PlatformNotSupportedException) + { + _supportsNativeWebSocket = false; + } + } + + return _supportsNativeWebSocket.Value; + } + } + + /// + /// Gets the web socket port number. + /// + /// The web socket port number. + public int WebSocketPortNumber + { + get { return SupportsNativeWebSocket ? Kernel.Configuration.HttpServerPortNumber : Kernel.Configuration.LegacyWebSocketPortNumber; } + } + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public TcpManager(IKernel kernel) + : base(kernel) + { + if (kernel.IsFirstRun) + { + RegisterServerWithAdministratorAccess(); + } + + ReloadUdpServer(); + ReloadHttpServer(); + + if (!SupportsNativeWebSocket) + { + ReloadExternalWebSocketServer(); + } + } + + /// + /// Starts the external web socket server. + /// + private void ReloadExternalWebSocketServer() + { + // Avoid windows firewall prompts in the ui + if (Kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeExternalWebSocketServer(); + + ExternalWebSocketServer = new WebSocketServer(Kernel.Configuration.LegacyWebSocketPortNumber, IPAddress.Any) + { + OnConnected = OnAlchemyWebSocketClientConnected, + TimeOut = TimeSpan.FromMinutes(60) + }; + + ExternalWebSocketServer.Start(); + + Logger.Info("Alchemy Web Socket Server started"); + } + + /// + /// Called when [alchemy web socket client connected]. + /// + /// The context. + private void OnAlchemyWebSocketClientConnected(UserContext context) + { + var connection = new WebSocketConnection(new AlchemyWebSocket(context), context.ClientAddress, ProcessWebSocketMessageReceived); + + _webSocketConnections.Add(connection); + } + + /// + /// Restarts the Http Server, or starts it if not currently running + /// + /// if set to true [register server on failure]. + public void ReloadHttpServer(bool registerServerOnFailure = true) + { + // Only reload if the port has changed, so that we don't disconnect any active users + if (HttpServer != null && HttpServer.UrlPrefix.Equals(Kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + DisposeHttpServer(); + + Logger.Info("Loading Http Server"); + + try + { + HttpServer = new HttpServer(Kernel.HttpServerUrlPrefix, "Media Browser", Kernel); + } + catch (HttpListenerException ex) + { + Logger.ErrorException("Error starting Http Server", ex); + + if (registerServerOnFailure) + { + RegisterServerWithAdministratorAccess(); + + // Don't get stuck in a loop + ReloadHttpServer(false); + + return; + } + + throw; + } + + HttpServer.WebSocketConnected += HttpServer_WebSocketConnected; + } + + /// + /// Handles the WebSocketConnected event of the HttpServer control. + /// + /// The source of the event. + /// The instance containing the event data. + void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) + { + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, ProcessWebSocketMessageReceived); + + _webSocketConnections.Add(connection); + } + + /// + /// Processes the web socket message received. + /// + /// The result. + private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + var tasks = Kernel.WebSocketListeners.Select(i => Task.Run(async () => + { + try + { + await i.ProcessMessage(result).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType); + } + })); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// + /// Starts or re-starts the udp server + /// + private void ReloadUdpServer() + { + // For now, there's no reason to keep reloading this over and over + if (UdpServer != null) + { + return; + } + + // Avoid windows firewall prompts in the ui + if (Kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeUdpServer(); + + try + { + // The port number can't be in configuration because we don't want it to ever change + UdpServer = new UdpServer(new IPEndPoint(IPAddress.Any, Kernel.UdpServerPortNumber)); + } + catch (SocketException ex) + { + Logger.ErrorException("Failed to start UDP Server", ex); + return; + } + + UdpListener = UdpServer.Subscribe(async res => + { + var expectedMessage = String.Format("who is MediaBrowser{0}?", Kernel.KernelContext); + var expectedMessageBytes = Encoding.UTF8.GetBytes(expectedMessage); + + if (expectedMessageBytes.SequenceEqual(res.Buffer)) + { + Logger.Info("Received UDP server request from " + res.RemoteEndPoint.ToString()); + + // Send a response back with our ip address and port + var response = String.Format("MediaBrowser{0}|{1}:{2}", Kernel.KernelContext, NetUtils.GetLocalIpAddress(), Kernel.UdpServerPortNumber); + + await UdpServer.SendAsync(response, res.RemoteEndPoint); + } + }); + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The data. + /// Task. + public void SendWebSocketMessage(string messageType, T data) + { + SendWebSocketMessage(messageType, () => data); + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The function that generates the data to send, if there are any connected clients + public void SendWebSocketMessage(string messageType, Func dataFunction) + { + Task.Run(async () => await SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None).ConfigureAwait(false)); + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The function that generates the data to send, if there are any connected clients + /// The cancellation token. + /// Task. + /// messageType + public async Task SendWebSocketMessageAsync(string messageType, Func dataFunction, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(messageType)) + { + throw new ArgumentNullException("messageType"); + } + + if (dataFunction == null) + { + throw new ArgumentNullException("dataFunction"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var connections = _webSocketConnections.Where(s => s.State == WebSocketState.Open).ToList(); + + if (connections.Count > 0) + { + Logger.Info("Sending web socket message {0}", messageType); + + var message = new WebSocketMessage { MessageType = messageType, Data = dataFunction() }; + var bytes = JsonSerializer.SerializeToBytes(message); + + var tasks = connections.Select(s => Task.Run(() => + { + try + { + s.SendAsync(bytes, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.ErrorException("Error sending web socket message {0} to {1}", ex, messageType, s.RemoteEndPoint); + } + })); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + + /// + /// Disposes the udp server + /// + private void DisposeUdpServer() + { + if (UdpServer != null) + { + UdpServer.Dispose(); + } + + if (UdpListener != null) + { + UdpListener.Dispose(); + } + } + + /// + /// Disposes the current HttpServer + /// + private void DisposeHttpServer() + { + foreach (var socket in _webSocketConnections) + { + // Dispose the connection + socket.Dispose(); + } + + _webSocketConnections.Clear(); + + if (HttpServer != null) + { + Logger.Info("Disposing Http Server"); + + HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected; + HttpServer.Dispose(); + } + + if (HttpListener != null) + { + HttpListener.Dispose(); + } + + DisposeExternalWebSocketServer(); + } + + /// + /// Registers the server with administrator access. + /// + private void RegisterServerWithAdministratorAccess() + { + // Create a temp file path to extract the bat file to + var tmpFile = Path.Combine(Kernel.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".bat"); + + // Extract the bat file + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Common.Kernel.RegisterServer.bat")) + { + using (var fileStream = File.Create(tmpFile)) + { + stream.CopyTo(fileStream); + } + } + + var startInfo = new ProcessStartInfo + { + FileName = tmpFile, + + Arguments = string.Format("{0} {1} {2} {3}", Kernel.Configuration.HttpServerPortNumber, + Kernel.HttpServerUrlPrefix, + Kernel.UdpServerPortNumber, + Kernel.Configuration.LegacyWebSocketPortNumber), + + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + Verb = "runas", + ErrorDialog = false + }; + + using (var process = Process.Start(startInfo)) + { + process.WaitForExit(); + } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool dispose) + { + if (dispose) + { + DisposeUdpServer(); + DisposeHttpServer(); + } + + base.Dispose(dispose); + } + + /// + /// Disposes the external web socket server. + /// + private void DisposeExternalWebSocketServer() + { + if (ExternalWebSocketServer != null) + { + ExternalWebSocketServer.Dispose(); + } + } + + /// + /// Called when [application configuration changed]. + /// + /// The old config. + /// The new config. + public void OnApplicationConfigurationChanged(BaseApplicationConfiguration oldConfig, BaseApplicationConfiguration newConfig) + { + if (oldConfig.HttpServerPortNumber != newConfig.HttpServerPortNumber) + { + ReloadHttpServer(); + } + + if (!SupportsNativeWebSocket && oldConfig.LegacyWebSocketPortNumber != newConfig.LegacyWebSocketPortNumber) + { + ReloadExternalWebSocketServer(); + } + } + } +} -- cgit v1.2.3