repo creation with initial code after cloning public repo
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SessionWebSocketListener.
|
||||
/// </summary>
|
||||
public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The timeout in seconds after which a WebSocket is considered to be lost.
|
||||
/// </summary>
|
||||
private const int WebSocketLostTimeout = 60;
|
||||
|
||||
/// <summary>
|
||||
/// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
|
||||
/// </summary>
|
||||
private const float IntervalFactor = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
|
||||
/// </summary>
|
||||
private const float ForceKeepAliveFactor = 0.75f;
|
||||
|
||||
/// <summary>
|
||||
/// The WebSocket watchlist.
|
||||
/// </summary>
|
||||
private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
|
||||
|
||||
/// <summary>
|
||||
/// Lock used for accessing the WebSockets watchlist.
|
||||
/// </summary>
|
||||
private readonly Lock _webSocketsLock = new();
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<SessionWebSocketListener> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The KeepAlive cancellation token.
|
||||
/// </summary>
|
||||
private System.Timers.Timer _keepAlive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public SessionWebSocketListener(
|
||||
ILogger<SessionWebSocketListener> logger,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_loggerFactory = loggerFactory;
|
||||
_keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
|
||||
{
|
||||
AutoReset = true,
|
||||
Enabled = false
|
||||
};
|
||||
_keepAlive.Elapsed += KeepAliveSockets;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_keepAlive is not null)
|
||||
{
|
||||
_keepAlive.Stop();
|
||||
_keepAlive.Elapsed -= KeepAliveSockets;
|
||||
_keepAlive.Dispose();
|
||||
_keepAlive = null!;
|
||||
}
|
||||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
foreach (var webSocket in _webSockets)
|
||||
{
|
||||
webSocket.Closed -= OnWebSocketClosed;
|
||||
}
|
||||
|
||||
_webSockets.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task ProcessMessageAsync(WebSocketMessageInfo message)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
|
||||
{
|
||||
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(false);
|
||||
EnsureController(session, connection);
|
||||
await KeepAliveWebSocket(connection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
|
||||
{
|
||||
var controllerInfo = session.EnsureController<WebSocketController>(
|
||||
s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
|
||||
|
||||
var controller = (WebSocketController)controllerInfo.Item1;
|
||||
controller.AddWebSocket(connection);
|
||||
|
||||
_sessionManager.OnSessionControllerConnected(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a WebSocket is closed.
|
||||
/// </summary>
|
||||
/// <param name="sender">The WebSocket.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void OnWebSocketClosed(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var webSocket = (IWebSocketConnection)sender;
|
||||
_logger.LogDebug("WebSocket {0} is closed.", webSocket);
|
||||
RemoveWebSocket(webSocket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WebSocket to the KeepAlive watchlist.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket to monitor.</param>
|
||||
private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
|
||||
{
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (!_webSockets.Add(webSocket))
|
||||
{
|
||||
_logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
webSocket.Closed += OnWebSocketClosed;
|
||||
webSocket.LastKeepAliveDate = DateTime.UtcNow;
|
||||
|
||||
_keepAlive.Start();
|
||||
}
|
||||
|
||||
// Notify WebSocket about timeout
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a WebSocket from the KeepAlive watchlist.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket to remove.</param>
|
||||
private void RemoveWebSocket(IWebSocketConnection webSocket)
|
||||
{
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (_webSockets.Remove(webSocket))
|
||||
{
|
||||
webSocket.Closed -= OnWebSocketClosed;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
|
||||
}
|
||||
|
||||
if (_webSockets.Count == 0)
|
||||
{
|
||||
_keepAlive.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks status of KeepAlive of WebSockets.
|
||||
/// </summary>
|
||||
private async void KeepAliveSockets(object? o, EventArgs? e)
|
||||
{
|
||||
List<IWebSocketConnection> inactive;
|
||||
List<IWebSocketConnection> lost;
|
||||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
_logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
|
||||
|
||||
inactive = _webSockets.Where(i =>
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
|
||||
return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
|
||||
}).ToList();
|
||||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||
}
|
||||
|
||||
if (inactive.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||
}
|
||||
|
||||
foreach (var webSocket in inactive)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
_logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
|
||||
lost.Add(webSocket);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (lost.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||
foreach (var webSocket in lost)
|
||||
{
|
||||
// TODO: handle session relative to the lost webSocket
|
||||
RemoveWebSocket(webSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ForceKeepAlive message to a WebSocket.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||
{
|
||||
await webSocket.SendAsync(
|
||||
new ForceKeepAliveMessage(WebSocketLostTimeout),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
|
||||
{
|
||||
private readonly ILogger<WebSocketController> _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly SessionInfo _session;
|
||||
|
||||
private readonly List<IWebSocketConnection> _sockets;
|
||||
private readonly ReaderWriterLockSlim _socketsLock;
|
||||
private bool _disposed = false;
|
||||
|
||||
public WebSocketController(
|
||||
ILogger<WebSocketController> logger,
|
||||
SessionInfo session,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
_sockets = new();
|
||||
_socketsLock = new();
|
||||
}
|
||||
|
||||
private bool HasOpenSockets
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterReadLock();
|
||||
return _sockets.Any(i => i.State == WebSocketState.Open);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsMediaControl => HasOpenSockets;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSessionActive => HasOpenSockets;
|
||||
|
||||
public void AddWebSocket(IWebSocketConnection connection)
|
||||
{
|
||||
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
_sockets.Add(connection);
|
||||
connection.Closed += OnConnectionClosed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnConnectionClosed(object? sender, EventArgs e)
|
||||
{
|
||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage<T>(
|
||||
SessionMessageType name,
|
||||
Guid messageId,
|
||||
T data,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
IWebSocketConnection? socket;
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterReadLock();
|
||||
socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitReadLock();
|
||||
}
|
||||
|
||||
if (socket is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return socket.SendAsync(
|
||||
new OutboundWebSocketMessage<T>
|
||||
{
|
||||
Data = data,
|
||||
MessageType = name,
|
||||
MessageId = messageId
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
socket.Dispose();
|
||||
}
|
||||
|
||||
_sockets.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_socketsLock.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_socketsLock.EnterWriteLock();
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
await socket.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_sockets.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_socketsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_socketsLock.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user