diff options
Diffstat (limited to 'MediaBrowser.Common.Implementations')
7 files changed, 816 insertions, 32 deletions
diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index c5af5059f..f5855bf75 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -151,21 +151,22 @@ namespace MediaBrowser.Common.Implementations /// </summary> /// <typeparam name="T"></typeparam> /// <param name="obj">The obj.</param> - protected void RegisterSingleInstance<T>(T obj) + /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param> + protected void RegisterSingleInstance<T>(T obj, bool manageLifetime = true) where T : class { Container.RegisterSingle(obj); - } - /// <summary> - /// Registers the specified func. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="func">The func.</param> - protected void Register<T>(Func<T> func) - where T : class - { - Container.Register(func); + if (manageLifetime) + { + var disposable = obj as IDisposable; + + if (disposable != null) + { + Logger.Info("Registering " + disposable.GetType().Name); + DisposableParts.Add(disposable); + } + } } /// <summary> @@ -206,16 +207,6 @@ namespace MediaBrowser.Common.Implementations } /// <summary> - /// Registers the specified service type. - /// </summary> - /// <param name="serviceType">Type of the service.</param> - /// <param name="implementation">Type of the concrete.</param> - protected void Register(Type serviceType, Type implementation) - { - Container.Register(serviceType, implementation); - } - - /// <summary> /// Loads the assembly. /// </summary> /// <param name="file">The file.</param> @@ -282,13 +273,17 @@ namespace MediaBrowser.Common.Implementations /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool dispose) { - foreach (var part in DisposableParts) + var type = GetType(); + + Logger.Info("Disposing " + type.Name); + + foreach (var part in DisposableParts.Distinct().Where(i => i.GetType() != type).ToList()) { + Logger.Info("Disposing " + part.GetType().Name); + part.Dispose(); } - var b = Container.GetCurrentRegistrations(); - DisposableParts.Clear(); } } diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index acd798d73..59ec01aaf 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -47,10 +47,7 @@ <Reference Include="System" /> <Reference Include="System.Configuration" /> <Reference Include="System.Core" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> @@ -69,6 +66,8 @@ <Compile Include="Serialization\JsonSerializer.cs" /> <Compile Include="Serialization\ProtobufSerializer.cs" /> <Compile Include="Serialization\XmlSerializer.cs" /> + <Compile Include="Server\ServerManager.cs" /> + <Compile Include="Server\WebSocketConnection.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> @@ -82,6 +81,7 @@ </ItemGroup> <ItemGroup> <None Include="packages.config" /> + <EmbeddedResource Include="Server\RegisterServer.bat" /> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 7fa30f4ae..320f440fc 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -49,6 +49,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks private ITaskManager TaskManager { get; set; } /// <summary> + /// Gets or sets the server manager. + /// </summary> + /// <value>The server manager.</value> + private IServerManager ServerManager { get; set; } + + /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. /// </summary> /// <param name="scheduledTask">The scheduled task.</param> @@ -56,13 +62,15 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <param name="taskManager">The task manager.</param> /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger) + /// <param name="serverManager">The server manager.</param> + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IServerManager serverManager) { ScheduledTask = scheduledTask; ApplicationPaths = applicationPaths; TaskManager = taskManager; JsonSerializer = jsonSerializer; Logger = logger; + ServerManager = serverManager; } /// <summary> @@ -302,7 +310,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks TaskCompletionStatus status; CurrentExecutionStartTime = DateTime.UtcNow; - //Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskBeginExecute", Name); + ServerManager.SendWebSocketMessage("ScheduledTaskBeginExecute", Name); try { @@ -324,7 +332,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks var startTime = CurrentExecutionStartTime; var endTime = DateTime.UtcNow; - //Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskEndExecute", LastExecutionResult); + ServerManager.SendWebSocketMessage("ScheduledTaskEndExecute", LastExecutionResult); progress.ProgressChanged -= progress_ProgressChanged; CurrentCancellationTokenSource.Dispose(); diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs index 4b61492d6..335820e1c 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -44,17 +44,25 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks private ILogger Logger { get; set; } /// <summary> + /// Gets or sets the server manager. + /// </summary> + /// <value>The server manager.</value> + private IServerManager ServerManager { get; set; } + + /// <summary> /// Initializes a new instance of the <see cref="TaskManager" /> class. /// </summary> /// <param name="applicationPaths">The application paths.</param> /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> + /// <param name="serverManager">The server manager.</param> /// <exception cref="System.ArgumentException">kernel</exception> - public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger) + public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IServerManager serverManager) { ApplicationPaths = applicationPaths; JsonSerializer = jsonSerializer; Logger = logger; + ServerManager = serverManager; ScheduledTasks = new IScheduledTaskWorker[] { }; } @@ -155,7 +163,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks { var myTasks = ScheduledTasks.ToList(); - myTasks.AddRange(tasks.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger))); + myTasks.AddRange(tasks.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, ServerManager))); ScheduledTasks = myTasks.ToArray(); } diff --git a/MediaBrowser.Common.Implementations/Server/RegisterServer.bat b/MediaBrowser.Common.Implementations/Server/RegisterServer.bat new file mode 100644 index 000000000..d762dfaf7 --- /dev/null +++ b/MediaBrowser.Common.Implementations/Server/RegisterServer.bat @@ -0,0 +1,28 @@ +rem %1 = http server port +rem %2 = http server url +rem %3 = udp server port +rem %4 = tcp server port (web socket) + +if [%1]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %1" protocol=TCP localport=%1 +netsh advfirewall firewall add rule name="Port %1" dir=in action=allow protocol=TCP localport=%1 + +if [%2]==[] GOTO DONE + +netsh http del urlacl url="%2" user="NT AUTHORITY\Authenticated Users" +netsh http add urlacl url="%2" user="NT AUTHORITY\Authenticated Users" + +if [%3]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %3" protocol=UDP localport=%3 +netsh advfirewall firewall add rule name="Port %3" dir=in action=allow protocol=UDP localport=%3 + +if [%4]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %4" protocol=TCP localport=%4 +netsh advfirewall firewall add rule name="Port %4" dir=in action=allow protocol=TCP localport=%4 + + +:DONE +Exit
\ No newline at end of file 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 +{ + /// <summary> + /// Manages the Http Server, Udp Server and WebSocket connections + /// </summary> + public class ServerManager : IServerManager, IDisposable + { + /// <summary> + /// This is the udp server used for server discovery by clients + /// </summary> + /// <value>The UDP server.</value> + private IUdpServer UdpServer { get; set; } + + /// <summary> + /// 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. + /// </summary> + /// <value>The HTTP server.</value> + private IHttpServer HttpServer { get; set; } + + /// <summary> + /// Gets or sets the json serializer. + /// </summary> + /// <value>The json serializer.</value> + private readonly IJsonSerializer _jsonSerializer; + + /// <summary> + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// </summary> + /// <value>The HTTP listener.</value> + private IDisposable HttpListener { get; set; } + + /// <summary> + /// The web socket connections + /// </summary> + private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); + + /// <summary> + /// Gets or sets the external web socket server. + /// </summary> + /// <value>The external web socket server.</value> + private IWebSocketServer ExternalWebSocketServer { get; set; } + + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// The _network manager + /// </summary> + private readonly INetworkManager _networkManager; + + /// <summary> + /// The _application host + /// </summary> + private readonly IApplicationHost _applicationHost; + + /// <summary> + /// The _kernel + /// </summary> + private readonly IKernel _kernel; + + /// <summary> + /// Gets a value indicating whether [supports web socket]. + /// </summary> + /// <value><c>true</c> if [supports web socket]; otherwise, <c>false</c>.</value> + public bool SupportsNativeWebSocket + { + get { return HttpServer != null && HttpServer.SupportsWebSockets; } + } + + /// <summary> + /// Gets the web socket port number. + /// </summary> + /// <value>The web socket port number.</value> + public int WebSocketPortNumber + { + get { return SupportsNativeWebSocket ? _kernel.Configuration.HttpServerPortNumber : _kernel.Configuration.LegacyWebSocketPortNumber; } + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServerManager" /> class. + /// </summary> + /// <param name="applicationHost">The application host.</param> + /// <param name="kernel">The kernel.</param> + /// <param name="networkManager">The network manager.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logger">The logger.</param> + /// <exception cref="System.ArgumentNullException">applicationHost</exception> + 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; + } + + /// <summary> + /// Starts this instance. + /// </summary> + public void Start() + { + if (_applicationHost.IsFirstRun) + { + RegisterServerWithAdministratorAccess(); + } + + ReloadUdpServer(); + ReloadHttpServer(); + + if (!SupportsNativeWebSocket) + { + ReloadExternalWebSocketServer(); + } + + _kernel.ConfigurationUpdated += _kernel_ConfigurationUpdated; + } + + /// <summary> + /// Starts the external web socket server. + /// </summary> + private void ReloadExternalWebSocketServer() + { + // Avoid windows firewall prompts in the ui + if (_kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeExternalWebSocketServer(); + + ExternalWebSocketServer = _applicationHost.Resolve<IWebSocketServer>(); + + ExternalWebSocketServer.Start(_kernel.Configuration.LegacyWebSocketPortNumber); + ExternalWebSocketServer.WebSocketConnected += HttpServer_WebSocketConnected; + } + + /// <summary> + /// Restarts the Http Server, or starts it if not currently running + /// </summary> + /// <param name="registerServerOnFailure">if set to <c>true</c> [register server on failure].</param> + 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<IHttpServer>(); + 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; + } + + /// <summary> + /// Handles the WebSocketConnected event of the HttpServer control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="WebSocketConnectEventArgs" /> instance containing the event data.</param> + void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) + { + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) { OnReceive = ProcessWebSocketMessageReceived }; + + _webSocketConnections.Add(connection); + } + + /// <summary> + /// Processes the web socket message received. + /// </summary> + /// <param name="result">The result.</param> + 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); + } + + /// <summary> + /// Starts or re-starts the udp server + /// </summary> + 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<IUdpServer>(); + UdpServer.Start(_kernel.UdpServerPortNumber); + } + catch (SocketException ex) + { + _logger.ErrorException("Failed to start UDP Server", ex); + return; + } + + UdpServer.MessageReceived += UdpServer_MessageReceived; + } + + /// <summary> + /// Handles the MessageReceived event of the UdpServer control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="UdpMessageReceivedEventArgs" /> instance containing the event data.</param> + 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); + } + } + + /// <summary> + /// Sends a message to all clients currently connected via a web socket + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="messageType">Type of the message.</param> + /// <param name="data">The data.</param> + /// <returns>Task.</returns> + public void SendWebSocketMessage<T>(string messageType, T data) + { + SendWebSocketMessage(messageType, () => data); + } + + /// <summary> + /// Sends a message to all clients currently connected via a web socket + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="messageType">Type of the message.</param> + /// <param name="dataFunction">The function that generates the data to send, if there are any connected clients</param> + public void SendWebSocketMessage<T>(string messageType, Func<T> dataFunction) + { + Task.Run(async () => await SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None).ConfigureAwait(false)); + } + + /// <summary> + /// Sends a message to all clients currently connected via a web socket + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="messageType">Type of the message.</param> + /// <param name="dataFunction">The function that generates the data to send, if there are any connected clients</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">messageType</exception> + public async Task SendWebSocketMessageAsync<T>(string messageType, Func<T> 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<T> { 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); + } + } + + /// <summary> + /// Disposes the udp server + /// </summary> + private void DisposeUdpServer() + { + if (UdpServer != null) + { + UdpServer.MessageReceived -= UdpServer_MessageReceived; + UdpServer.Dispose(); + } + } + + /// <summary> + /// Disposes the current HttpServer + /// </summary> + 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(); + } + + /// <summary> + /// Registers the server with administrator access. + /// </summary> + 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(); + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + DisposeUdpServer(); + DisposeHttpServer(); + } + } + + /// <summary> + /// Disposes the external web socket server. + /// </summary> + private void DisposeExternalWebSocketServer() + { + if (ExternalWebSocketServer != null) + { + ExternalWebSocketServer.Dispose(); + } + } + + /// <summary> + /// Handles the ConfigurationUpdated event of the _kernel control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> + /// <exception cref="System.NotImplementedException"></exception> + 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(); + } + } + } +} diff --git a/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs b/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs new file mode 100644 index 000000000..b8766523c --- /dev/null +++ b/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs @@ -0,0 +1,225 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.Server +{ + /// <summary> + /// Class WebSocketConnection + /// </summary> + public class WebSocketConnection : IWebSocketConnection + { + /// <summary> + /// The _socket + /// </summary> + private readonly IWebSocket _socket; + + /// <summary> + /// The _remote end point + /// </summary> + public string RemoteEndPoint { get; private set; } + + /// <summary> + /// The _cancellation token source + /// </summary> + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// <summary> + /// The _send semaphore + /// </summary> + private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1,1); + + /// <summary> + /// The logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// The _json serializer + /// </summary> + private readonly IJsonSerializer _jsonSerializer; + + /// <summary> + /// Gets or sets the receive action. + /// </summary> + /// <value>The receive action.</value> + public Action<WebSocketMessageInfo> OnReceive { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketConnection" /> class. + /// </summary> + /// <param name="socket">The socket.</param> + /// <param name="remoteEndPoint">The remote end point.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logger">The logger.</param> + /// <exception cref="System.ArgumentNullException">socket</exception> + public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + if (string.IsNullOrEmpty(remoteEndPoint)) + { + throw new ArgumentNullException("remoteEndPoint"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _jsonSerializer = jsonSerializer; + _socket = socket; + _socket.OnReceiveDelegate = OnReceiveInternal; + RemoteEndPoint = remoteEndPoint; + _logger = logger; + } + + /// <summary> + /// Called when [receive]. + /// </summary> + /// <param name="bytes">The bytes.</param> + private void OnReceiveInternal(byte[] bytes) + { + if (OnReceive == null) + { + return; + } + try + { + WebSocketMessageInfo info; + + using (var memoryStream = new MemoryStream(bytes)) + { + info = (WebSocketMessageInfo)_jsonSerializer.DeserializeFromStream(memoryStream, typeof(WebSocketMessageInfo)); + } + + info.Connection = this; + + OnReceive(info); + } + catch (Exception ex) + { + _logger.ErrorException("Error processing web socket message", ex); + } + } + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">message</exception> + public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + var bytes = _jsonSerializer.SerializeToBytes(message); + + return SendAsync(bytes, cancellationToken); + } + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="buffer">The buffer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendAsync(byte[] buffer, CancellationToken cancellationToken) + { + return SendAsync(buffer, WebSocketMessageType.Text, cancellationToken); + } + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="buffer">The buffer.</param> + /// <param name="type">The type.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">buffer</exception> + public async Task SendAsync(byte[] buffer, WebSocketMessageType type, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Per msdn docs, attempting to send simultaneous messages will result in one failing. + // This should help us workaround that and ensure all messages get sent + await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await _socket.SendAsync(buffer, type, true, cancellationToken); + } + catch (OperationCanceledException) + { + _logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint); + + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint); + + throw; + } + finally + { + _sendSemaphore.Release(); + } + } + + /// <summary> + /// Gets the state. + /// </summary> + /// <value>The state.</value> + public WebSocketState State + { + get { return _socket.State; } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _cancellationTokenSource.Dispose(); + _socket.Dispose(); + } + } + } +} |
