< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Session.SessionWebSocketListener
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
Line coverage
46%
Covered lines: 22
Uncovered lines: 25
Coverable lines: 47
Total lines: 286
Line coverage: 46.8%
Branch coverage
30%
Covered branches: 3
Total branches: 10
Branch coverage: 30%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Dispose()75%4.01490.9%
ProcessMessageAsync(...)100%210%
EnsureController(...)100%210%
OnWebSocketClosed(...)0%620%
RemoveWebSocket(...)0%2040%
SendForceKeepAlive(...)100%210%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Session/SessionWebSocketListener.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Net.WebSockets;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Api.Extensions;
 8using MediaBrowser.Controller.Net;
 9using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 10using MediaBrowser.Controller.Session;
 11using Microsoft.AspNetCore.Http;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Emby.Server.Implementations.Session
 15{
 16    /// <summary>
 17    /// Class SessionWebSocketListener.
 18    /// </summary>
 19    public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
 20    {
 21        /// <summary>
 22        /// The timeout in seconds after which a WebSocket is considered to be lost.
 23        /// </summary>
 24        private const int WebSocketLostTimeout = 60;
 25
 26        /// <summary>
 27        /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
 28        /// </summary>
 29        private const float IntervalFactor = 0.2f;
 30
 31        /// <summary>
 32        /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
 33        /// </summary>
 34        private const float ForceKeepAliveFactor = 0.75f;
 35
 36        /// <summary>
 37        /// The WebSocket watchlist.
 38        /// </summary>
 2039        private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
 40
 41        /// <summary>
 42        /// Lock used for accessing the WebSockets watchlist.
 43        /// </summary>
 2044        private readonly object _webSocketsLock = new object();
 45
 46        private readonly ISessionManager _sessionManager;
 47        private readonly ILogger<SessionWebSocketListener> _logger;
 48        private readonly ILoggerFactory _loggerFactory;
 49
 50        /// <summary>
 51        /// The KeepAlive cancellation token.
 52        /// </summary>
 53        private System.Timers.Timer _keepAlive;
 54
 55        /// <summary>
 56        /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
 57        /// </summary>
 58        /// <param name="logger">The logger.</param>
 59        /// <param name="sessionManager">The session manager.</param>
 60        /// <param name="loggerFactory">The logger factory.</param>
 61        public SessionWebSocketListener(
 62            ILogger<SessionWebSocketListener> logger,
 63            ISessionManager sessionManager,
 64            ILoggerFactory loggerFactory)
 65        {
 2066            _logger = logger;
 2067            _sessionManager = sessionManager;
 2068            _loggerFactory = loggerFactory;
 2069            _keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
 2070            {
 2071                AutoReset = true,
 2072                Enabled = false
 2073            };
 2074            _keepAlive.Elapsed += KeepAliveSockets;
 2075        }
 76
 77        /// <inheritdoc />
 78        public void Dispose()
 79        {
 2080            if (_keepAlive is not null)
 81            {
 2082                _keepAlive.Stop();
 2083                _keepAlive.Elapsed -= KeepAliveSockets;
 2084                _keepAlive.Dispose();
 2085                _keepAlive = null!;
 86            }
 87
 2088            lock (_webSocketsLock)
 89            {
 4090                foreach (var webSocket in _webSockets)
 91                {
 092                    webSocket.Closed -= OnWebSocketClosed;
 93                }
 94
 2095                _webSockets.Clear();
 2096            }
 2097        }
 98
 99        /// <summary>
 100        /// Processes the message.
 101        /// </summary>
 102        /// <param name="message">The message.</param>
 103        /// <returns>Task.</returns>
 104        public Task ProcessMessageAsync(WebSocketMessageInfo message)
 0105            => Task.CompletedTask;
 106
 107        /// <inheritdoc />
 108        public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
 109        {
 110            var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false);
 111            if (session is not null)
 112            {
 113                EnsureController(session, connection);
 114                await KeepAliveWebSocket(connection).ConfigureAwait(false);
 115            }
 116            else
 117            {
 118                _logger.LogWarning("Unable to determine session based on query string: {0}", httpContext.Request.QuerySt
 119            }
 120        }
 121
 122        private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
 123        {
 124            if (!httpContext.User.Identity?.IsAuthenticated ?? false)
 125            {
 126                return null;
 127            }
 128
 129            var deviceId = httpContext.User.GetDeviceId();
 130            if (httpContext.Request.Query.TryGetValue("deviceId", out var queryDeviceId))
 131            {
 132                deviceId = queryDeviceId;
 133            }
 134
 135            return await _sessionManager.GetSessionByAuthenticationToken(httpContext.User.GetToken(), deviceId, remoteEn
 136                .ConfigureAwait(false);
 137        }
 138
 139        private void EnsureController(SessionInfo session, IWebSocketConnection connection)
 140        {
 0141            var controllerInfo = session.EnsureController<WebSocketController>(
 0142                s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
 143
 0144            var controller = (WebSocketController)controllerInfo.Item1;
 0145            controller.AddWebSocket(connection);
 146
 0147            _sessionManager.OnSessionControllerConnected(session);
 0148        }
 149
 150        /// <summary>
 151        /// Called when a WebSocket is closed.
 152        /// </summary>
 153        /// <param name="sender">The WebSocket.</param>
 154        /// <param name="e">The event arguments.</param>
 155        private void OnWebSocketClosed(object? sender, EventArgs e)
 156        {
 0157            if (sender is null)
 158            {
 0159                return;
 160            }
 161
 0162            var webSocket = (IWebSocketConnection)sender;
 0163            _logger.LogDebug("WebSocket {0} is closed.", webSocket);
 0164            RemoveWebSocket(webSocket);
 0165        }
 166
 167        /// <summary>
 168        /// Adds a WebSocket to the KeepAlive watchlist.
 169        /// </summary>
 170        /// <param name="webSocket">The WebSocket to monitor.</param>
 171        private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
 172        {
 173            lock (_webSocketsLock)
 174            {
 175                if (!_webSockets.Add(webSocket))
 176                {
 177                    _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
 178                    return;
 179                }
 180
 181                webSocket.Closed += OnWebSocketClosed;
 182                webSocket.LastKeepAliveDate = DateTime.UtcNow;
 183
 184                _keepAlive.Start();
 185            }
 186
 187            // Notify WebSocket about timeout
 188            try
 189            {
 190                await SendForceKeepAlive(webSocket).ConfigureAwait(false);
 191            }
 192            catch (WebSocketException exception)
 193            {
 194                _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
 195            }
 196        }
 197
 198        /// <summary>
 199        /// Removes a WebSocket from the KeepAlive watchlist.
 200        /// </summary>
 201        /// <param name="webSocket">The WebSocket to remove.</param>
 202        private void RemoveWebSocket(IWebSocketConnection webSocket)
 203        {
 0204            lock (_webSocketsLock)
 205            {
 0206                if (_webSockets.Remove(webSocket))
 207                {
 0208                    webSocket.Closed -= OnWebSocketClosed;
 209                }
 210                else
 211                {
 0212                    _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
 213                }
 214
 0215                if (_webSockets.Count == 0)
 216                {
 0217                    _keepAlive.Stop();
 218                }
 0219            }
 0220        }
 221
 222        /// <summary>
 223        /// Checks status of KeepAlive of WebSockets.
 224        /// </summary>
 225        private async void KeepAliveSockets(object? o, EventArgs? e)
 226        {
 227            List<IWebSocketConnection> inactive;
 228            List<IWebSocketConnection> lost;
 229
 230            lock (_webSocketsLock)
 231            {
 232                _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
 233
 234                inactive = _webSockets.Where(i =>
 235                {
 236                    var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
 237                    return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
 238                }).ToList();
 239                lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeo
 240            }
 241
 242            if (inactive.Count > 0)
 243            {
 244                _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
 245            }
 246
 247            foreach (var webSocket in inactive)
 248            {
 249                try
 250                {
 251                    await SendForceKeepAlive(webSocket).ConfigureAwait(false);
 252                }
 253                catch (WebSocketException exception)
 254                {
 255                    _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
 256                    lost.Add(webSocket);
 257                }
 258            }
 259
 260            lock (_webSocketsLock)
 261            {
 262                if (lost.Count > 0)
 263                {
 264                    _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
 265                    foreach (var webSocket in lost)
 266                    {
 267                        // TODO: handle session relative to the lost webSocket
 268                        RemoveWebSocket(webSocket);
 269                    }
 270                }
 271            }
 272        }
 273
 274        /// <summary>
 275        /// Sends a ForceKeepAlive message to a WebSocket.
 276        /// </summary>
 277        /// <param name="webSocket">The WebSocket.</param>
 278        /// <returns>Task.</returns>
 279        private Task SendForceKeepAlive(IWebSocketConnection webSocket)
 280        {
 0281            return webSocket.SendAsync(
 0282                new ForceKeepAliveMessage(WebSocketLostTimeout),
 0283                CancellationToken.None);
 284        }
 285    }
 286}