From 6efd22a3d22f8d87ad17da3a1e47ca26c5bb09f2 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Tue, 26 Feb 2013 11:10:55 -0500 Subject: added a shutdown api method, font size fix and other decouplings --- .../Server/ServerManager.cs | 520 +++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 MediaBrowser.Common.Implementations/Server/ServerManager.cs (limited to 'MediaBrowser.Common.Implementations/Server/ServerManager.cs') diff --git a/MediaBrowser.Common.Implementations/Server/ServerManager.cs b/MediaBrowser.Common.Implementations/Server/ServerManager.cs new file mode 100644 index 000000000..04747bad6 --- /dev/null +++ b/MediaBrowser.Common.Implementations/Server/ServerManager.cs @@ -0,0 +1,520 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.Server +{ + /// + /// Manages the Http Server, Udp Server and WebSocket connections + /// + public class ServerManager : IServerManager, IDisposable + { + /// + /// This is the udp server used for server discovery by clients + /// + /// The UDP server. + private IUdpServer UdpServer { 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. + private IHttpServer HttpServer { get; set; } + + /// + /// Gets or sets the json serializer. + /// + /// The json serializer. + private readonly IJsonSerializer _jsonSerializer; + + /// + /// 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 IWebSocketServer ExternalWebSocketServer { get; set; } + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// The _network manager + /// + private readonly INetworkManager _networkManager; + + /// + /// The _application host + /// + private readonly IApplicationHost _applicationHost; + + /// + /// The _kernel + /// + private readonly IKernel _kernel; + + /// + /// Gets a value indicating whether [supports web socket]. + /// + /// true if [supports web socket]; otherwise, false. + public bool SupportsNativeWebSocket + { + get { return HttpServer != null && HttpServer.SupportsWebSockets; } + } + + /// + /// 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 application host. + /// The kernel. + /// The network manager. + /// The json serializer. + /// The logger. + /// applicationHost + public ServerManager(IApplicationHost applicationHost, IKernel kernel, INetworkManager networkManager, IJsonSerializer jsonSerializer, ILogger logger) + { + if (applicationHost == null) + { + throw new ArgumentNullException("applicationHost"); + } + if (kernel == null) + { + throw new ArgumentNullException("kernel"); + } + if (networkManager == null) + { + throw new ArgumentNullException("networkManager"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + _jsonSerializer = jsonSerializer; + _kernel = kernel; + _applicationHost = applicationHost; + _networkManager = networkManager; + } + + /// + /// Starts this instance. + /// + public void Start() + { + if (_applicationHost.IsFirstRun) + { + RegisterServerWithAdministratorAccess(); + } + + ReloadUdpServer(); + ReloadHttpServer(); + + if (!SupportsNativeWebSocket) + { + ReloadExternalWebSocketServer(); + } + + _kernel.ConfigurationUpdated += _kernel_ConfigurationUpdated; + } + + /// + /// Starts the external web socket server. + /// + private void ReloadExternalWebSocketServer() + { + // Avoid windows firewall prompts in the ui + if (_kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeExternalWebSocketServer(); + + ExternalWebSocketServer = _applicationHost.Resolve(); + + ExternalWebSocketServer.Start(_kernel.Configuration.LegacyWebSocketPortNumber); + ExternalWebSocketServer.WebSocketConnected += HttpServer_WebSocketConnected; + } + + /// + /// Restarts the Http Server, or starts it if not currently running + /// + /// if set to true [register server on failure]. + private 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 = _applicationHost.Resolve(); + HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; + HttpServer.Start(_kernel.HttpServerUrlPrefix); + } + 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, _jsonSerializer, _logger) { OnReceive = 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 = _applicationHost.Resolve(); + UdpServer.Start(_kernel.UdpServerPortNumber); + } + catch (SocketException ex) + { + _logger.ErrorException("Failed to start UDP Server", ex); + return; + } + + UdpServer.MessageReceived += UdpServer_MessageReceived; + } + + /// + /// Handles the MessageReceived event of the UdpServer control. + /// + /// The source of the event. + /// The instance containing the event data. + async void UdpServer_MessageReceived(object sender, UdpMessageReceivedEventArgs e) + { + var expectedMessage = String.Format("who is MediaBrowser{0}?", _kernel.KernelContext); + var expectedMessageBytes = Encoding.UTF8.GetBytes(expectedMessage); + + if (expectedMessageBytes.SequenceEqual(e.Bytes)) + { + _logger.Info("Received UDP server request from " + e.RemoteEndPoint); + + // Send a response back with our ip address and port + var response = String.Format("MediaBrowser{0}|{1}:{2}", _kernel.KernelContext, _networkManager.GetLocalIpAddress(), _kernel.UdpServerPortNumber); + + await UdpServer.SendAsync(Encoding.UTF8.GetBytes(response), e.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.MessageReceived -= UdpServer_MessageReceived; + UdpServer.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.Implementations.Server.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(); + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + DisposeUdpServer(); + DisposeHttpServer(); + } + } + + /// + /// Disposes the external web socket server. + /// + private void DisposeExternalWebSocketServer() + { + if (ExternalWebSocketServer != null) + { + ExternalWebSocketServer.Dispose(); + } + } + + /// + /// Handles the ConfigurationUpdated event of the _kernel control. + /// + /// The source of the event. + /// The instance containing the event data. + /// + void _kernel_ConfigurationUpdated(object sender, EventArgs e) + { + HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; + + if (!string.Equals(HttpServer.UrlPrefix, _kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + ReloadHttpServer(); + } + + if (!SupportsNativeWebSocket && ExternalWebSocketServer != null && ExternalWebSocketServer.Port != _kernel.Configuration.LegacyWebSocketPortNumber) + { + ReloadExternalWebSocketServer(); + } + } + } +} -- cgit v1.2.3