#nullable disable #pragma warning disable CS1591, SA1306, SA1401 using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Net { /// /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received. /// /// The type of the T return data type. /// The type of the T state type. public abstract class BasePeriodicWebSocketListener : IWebSocketListener, IAsyncDisposable where TStateType : WebSocketListenerState, new() where TReturnDataType : class { private readonly Channel _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = false }); private readonly SemaphoreSlim _lock = new(1, 1); /// /// The _active connections. /// private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new(); /// /// The logger. /// protected readonly ILogger> Logger; private readonly Task _messageConsumerTask; protected BasePeriodicWebSocketListener(ILogger> logger) { ArgumentNullException.ThrowIfNull(logger); Logger = logger; _messageConsumerTask = HandleMessages(); } /// /// Gets the type used for the messages sent to the client. /// /// The type. protected abstract SessionMessageType Type { get; } /// /// Gets the message type received from the client to start sending messages. /// /// The type. protected abstract SessionMessageType StartType { get; } /// /// Gets the message type received from the client to stop sending messages. /// /// The type. protected abstract SessionMessageType StopType { get; } /// /// Gets the data to send. /// /// Task{`1}. protected abstract Task GetDataToSend(); /// /// Processes the message. /// /// The message. /// Task. public Task ProcessMessageAsync(WebSocketMessageInfo message) { ArgumentNullException.ThrowIfNull(message); if (message.MessageType == StartType) { Start(message); } if (message.MessageType == StopType) { Stop(message); } return Task.CompletedTask; } /// public Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext) => Task.CompletedTask; /// /// Starts sending messages over a web socket. /// /// The message. protected virtual void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture); var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture); var cancellationTokenSource = new CancellationTokenSource(); Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name); var state = new TStateType { IntervalMs = periodMs, InitialDelayMs = dueTimeMs }; _lock.Wait(); try { _activeConnections.Add((message.Connection, cancellationTokenSource, state)); } finally { _lock.Release(); } } protected void SendData(bool force) { _channel.Writer.TryWrite(force); } private async Task HandleMessages() { while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false)) { while (_channel.Reader.TryRead(out var force)) { try { (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples; var now = DateTime.UtcNow; await _lock.WaitAsync().ConfigureAwait(false); try { if (_activeConnections.Count == 0) { continue; } tuples = _activeConnections .Where(c => { if (c.Connection.State != WebSocketState.Open || c.CancellationTokenSource.IsCancellationRequested) { return false; } var state = c.State; return force || (now - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs; }) .ToArray(); } finally { _lock.Release(); } if (tuples.Length == 0) { continue; } var data = await GetDataToSend().ConfigureAwait(false); if (data is null) { continue; } IEnumerable GetTasks() { foreach (var tuple in tuples) { yield return SendDataInternal(data, tuple); } } await Task.WhenAll(GetTasks()).ConfigureAwait(false); } catch (Exception ex) { Logger.LogError(ex, "Failed to send updates to websockets"); } } } } private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple) { try { var (connection, cts, state) = tuple; var cancellationToken = cts.Token; await connection.SendAsync( new OutboundWebSocketMessage { MessageType = Type, Data = data }, cancellationToken).ConfigureAwait(false); state.DateLastSendUtc = DateTime.UtcNow; } catch (OperationCanceledException) { if (tuple.CancellationTokenSource.IsCancellationRequested) { DisposeConnection(tuple); } } catch (Exception ex) { Logger.LogError(ex, "Error sending web socket message {Name}", Type); DisposeConnection(tuple); } } /// /// Stops sending messages over a web socket. /// /// The message. private void Stop(WebSocketMessageInfo message) { _lock.Wait(); try { var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection); if (connection != default) { DisposeConnection(connection); } } finally { _lock.Release(); } } /// /// Disposes the connection. /// /// The connection. private void DisposeConnection((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) connection) { Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Connection.RemoteEndPoint, GetType().Name); // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... // connection.Item1.Dispose(); try { connection.CancellationTokenSource.Cancel(); connection.CancellationTokenSource.Dispose(); } catch (ObjectDisposedException ex) { // TODO Investigate and properly fix. Logger.LogError(ex, "Object Disposed"); } catch (Exception ex) { // TODO Investigate and properly fix. Logger.LogError(ex, "Error disposing websocket"); } _lock.Wait(); try { _activeConnections.Remove(connection); } finally { _lock.Release(); } } protected virtual async ValueTask DisposeAsyncCore() { try { _channel.Writer.TryComplete(); await _messageConsumerTask.ConfigureAwait(false); } catch (Exception ex) { Logger.LogError(ex, "Disposing the message consumer failed"); } await _lock.WaitAsync().ConfigureAwait(false); try { foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } } finally { _lock.Release(); } } /// public async ValueTask DisposeAsync() { await DisposeAsyncCore().ConfigureAwait(false); GC.SuppressFinalize(this); } } }