< 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
73%
Covered lines: 71
Uncovered lines: 25
Coverable lines: 96
Total lines: 281
Line coverage: 73.9%
Branch coverage
60%
Covered branches: 12
Total branches: 20
Branch coverage: 60%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 AM Line coverage: 50% (22/44) Branch coverage: 30% (3/10) Total lines: 2684/19/2026 - 12:14:27 AM Line coverage: 24.1% (22/91) Branch coverage: 15% (3/20) Total lines: 2685/20/2026 - 12:15:44 AM Line coverage: 24.1% (22/91) Branch coverage: 10% (2/20) Total lines: 2687/3/2026 - 12:15:32 AM Line coverage: 73.9% (71/96) Branch coverage: 60% (12/20) Total lines: 281 3/26/2026 - 12:14:14 AM Line coverage: 50% (22/44) Branch coverage: 30% (3/10) Total lines: 2684/19/2026 - 12:14:27 AM Line coverage: 24.1% (22/91) Branch coverage: 15% (3/20) Total lines: 2685/20/2026 - 12:15:44 AM Line coverage: 24.1% (22/91) Branch coverage: 10% (2/20) Total lines: 2687/3/2026 - 12:15:32 AM Line coverage: 73.9% (71/96) Branch coverage: 60% (12/20) Total lines: 281

Coverage delta

Coverage delta 50 -50

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Dispose()50%4490%
ProcessMessageAsync(...)100%210%
ProcessWebSocketConnectedAsync()100%11100%
EnsureController(...)100%11100%
OnWebSocketClosed(...)0%620%
KeepAliveWebSocket()50%2264.28%
RemoveWebSocket(...)75%4487.5%
KeepAliveSockets()75%11863.33%
SendForceKeepAlive()100%11100%

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 Jellyfin.Api.Helpers;
 9using MediaBrowser.Controller.Library;
 10using MediaBrowser.Controller.Net;
 11using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 12using MediaBrowser.Controller.Session;
 13using Microsoft.AspNetCore.Http;
 14using Microsoft.Extensions.Logging;
 15
 16namespace Emby.Server.Implementations.Session
 17{
 18    /// <summary>
 19    /// Class SessionWebSocketListener.
 20    /// </summary>
 21    public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
 22    {
 23        /// <summary>
 24        /// The timeout in seconds after which a WebSocket is considered to be lost.
 25        /// </summary>
 26        private const int WebSocketLostTimeout = 60;
 27
 28        /// <summary>
 29        /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
 30        /// </summary>
 31        private const float IntervalFactor = 0.2f;
 32
 33        /// <summary>
 34        /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
 35        /// </summary>
 36        private const float ForceKeepAliveFactor = 0.75f;
 37
 38        /// <summary>
 39        /// The WebSocket watchlist.
 40        /// </summary>
 2041        private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
 42
 43        /// <summary>
 44        /// Lock used for accessing the WebSockets watchlist.
 45        /// </summary>
 2046        private readonly Lock _webSocketsLock = new();
 47
 48        private readonly ISessionManager _sessionManager;
 49        private readonly IUserManager _userManager;
 50        private readonly ILogger<SessionWebSocketListener> _logger;
 51        private readonly ILoggerFactory _loggerFactory;
 52
 53        /// <summary>
 54        /// The KeepAlive cancellation token.
 55        /// </summary>
 56        private System.Timers.Timer _keepAlive;
 57
 58        /// <summary>
 59        /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
 60        /// </summary>
 61        /// <param name="logger">The logger.</param>
 62        /// <param name="sessionManager">The session manager.</param>
 63        /// <param name="userManager">The user manager.</param>
 64        /// <param name="loggerFactory">The logger factory.</param>
 65        public SessionWebSocketListener(
 66            ILogger<SessionWebSocketListener> logger,
 67            ISessionManager sessionManager,
 68            IUserManager userManager,
 69            ILoggerFactory loggerFactory)
 70        {
 2071            _logger = logger;
 2072            _sessionManager = sessionManager;
 2073            _userManager = userManager;
 2074            _loggerFactory = loggerFactory;
 2075            _keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
 2076            {
 2077                AutoReset = true,
 2078                Enabled = false
 2079            };
 2080            _keepAlive.Elapsed += KeepAliveSockets;
 2081        }
 82
 83        /// <inheritdoc />
 84        public void Dispose()
 85        {
 2086            if (_keepAlive is not null)
 87            {
 2088                _keepAlive.Stop();
 2089                _keepAlive.Elapsed -= KeepAliveSockets;
 2090                _keepAlive.Dispose();
 2091                _keepAlive = null!;
 92            }
 93
 94            lock (_webSocketsLock)
 95            {
 4096                foreach (var webSocket in _webSockets)
 97                {
 098                    webSocket.Closed -= OnWebSocketClosed;
 99                }
 100
 20101                _webSockets.Clear();
 20102            }
 20103        }
 104
 105        /// <summary>
 106        /// Processes the message.
 107        /// </summary>
 108        /// <param name="message">The message.</param>
 109        /// <returns>Task.</returns>
 110        public Task ProcessMessageAsync(WebSocketMessageInfo message)
 0111            => Task.CompletedTask;
 112
 113        /// <inheritdoc />
 114        public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
 115        {
 1116            var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(fal
 1117            EnsureController(session, connection);
 1118            await KeepAliveWebSocket(connection).ConfigureAwait(false);
 1119        }
 120
 121        private void EnsureController(SessionInfo session, IWebSocketConnection connection)
 122        {
 1123            var controllerInfo = session.EnsureController<WebSocketController>(
 1124                s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
 125
 1126            var controller = (WebSocketController)controllerInfo.Item1;
 1127            controller.AddWebSocket(connection);
 128
 1129            _sessionManager.OnSessionControllerConnected(session);
 1130        }
 131
 132        /// <summary>
 133        /// Called when a WebSocket is closed.
 134        /// </summary>
 135        /// <param name="sender">The WebSocket.</param>
 136        /// <param name="e">The event arguments.</param>
 137        private void OnWebSocketClosed(object? sender, EventArgs e)
 138        {
 0139            if (sender is null)
 140            {
 0141                return;
 142            }
 143
 0144            var webSocket = (IWebSocketConnection)sender;
 0145            _logger.LogDebug("WebSocket {0} is closed.", webSocket);
 0146            RemoveWebSocket(webSocket);
 0147        }
 148
 149        /// <summary>
 150        /// Adds a WebSocket to the KeepAlive watchlist.
 151        /// </summary>
 152        /// <param name="webSocket">The WebSocket to monitor.</param>
 153        private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
 1154        {
 155            lock (_webSocketsLock)
 156            {
 1157                if (!_webSockets.Add(webSocket))
 158                {
 0159                    _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
 0160                    return;
 161                }
 162
 1163                webSocket.Closed += OnWebSocketClosed;
 1164                webSocket.LastKeepAliveDate = DateTime.UtcNow;
 165
 1166                _keepAlive.Start();
 1167            }
 168
 169            // Notify WebSocket about timeout
 170            try
 171            {
 1172                await SendForceKeepAlive(webSocket).ConfigureAwait(false);
 1173            }
 0174            catch (WebSocketException exception)
 175            {
 0176                _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
 0177            }
 1178        }
 179
 180        /// <summary>
 181        /// Removes a WebSocket from the KeepAlive watchlist.
 182        /// </summary>
 183        /// <param name="webSocket">The WebSocket to remove.</param>
 184        private void RemoveWebSocket(IWebSocketConnection webSocket)
 1185        {
 186            lock (_webSocketsLock)
 187            {
 1188                if (_webSockets.Remove(webSocket))
 189                {
 1190                    webSocket.Closed -= OnWebSocketClosed;
 191                }
 192                else
 193                {
 0194                    _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
 195                }
 196
 1197                if (_webSockets.Count == 0)
 198                {
 1199                    _keepAlive.Stop();
 200                }
 1201            }
 1202        }
 203
 204        /// <summary>
 205        /// Checks status of KeepAlive of WebSockets.
 206        /// </summary>
 207        private async void KeepAliveSockets(object? o, EventArgs? e)
 1208        {
 209            List<IWebSocketConnection> inactive;
 210            List<IWebSocketConnection> lost;
 211
 212            lock (_webSocketsLock)
 213            {
 1214                _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
 215
 1216                inactive = _webSockets.Where(i =>
 1217                {
 1218                    var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
 1219                    return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
 1220                }).ToList();
 1221                lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeo
 1222            }
 223
 1224            if (inactive.Count > 0)
 225            {
 0226                _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
 227            }
 228
 2229            foreach (var webSocket in inactive)
 230            {
 231                try
 232                {
 0233                    await SendForceKeepAlive(webSocket).ConfigureAwait(false);
 0234                }
 0235                catch (WebSocketException exception)
 236                {
 0237                    _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
 0238                    lost.Add(webSocket);
 0239                }
 0240            }
 241
 242            lock (_webSocketsLock)
 243            {
 1244                if (lost.Count > 0)
 245                {
 1246                    _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
 4247                    foreach (var webSocket in lost)
 248                    {
 1249                        RemoveWebSocket(webSocket);
 250
 251                        // The connection stopped answering keep-alives, so a close frame will
 252                        // never arrive and the pending receive loop would hang forever, keeping
 253                        // the session (and e.g. its SyncPlay group membership) alive. Disposing
 254                        // the connection aborts the receive loop, which raises Closed and lets
 255                        // the session end normally.
 256                        try
 257                        {
 1258                            webSocket.Dispose();
 1259                        }
 0260                        catch (Exception exception)
 261                        {
 0262                            _logger.LogWarning(exception, "Error disposing lost WebSocket from {RemoteEndPoint}.", webSo
 0263                        }
 264                    }
 265                }
 1266            }
 1267        }
 268
 269        /// <summary>
 270        /// Sends a ForceKeepAlive message to a WebSocket.
 271        /// </summary>
 272        /// <param name="webSocket">The WebSocket.</param>
 273        /// <returns>Task.</returns>
 274        private async Task SendForceKeepAlive(IWebSocketConnection webSocket)
 275        {
 1276            await webSocket.SendAsync(
 1277                new ForceKeepAliveMessage(WebSocketLostTimeout),
 1278                CancellationToken.None).ConfigureAwait(false);
 1279        }
 280    }
 281}