using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Common.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, IDisposable where TStateType : class, new() where TReturnDataType : class { /// /// The _active connections /// protected readonly List> ActiveConnections = new List>(); /// /// Gets the name. /// /// The name. protected abstract string Name { get; } /// /// Gets the data to send. /// /// The state. /// Task{`1}. protected abstract Task GetDataToSend(TStateType state); /// /// The logger /// protected ILogger Logger; /// /// Initializes a new instance of the class. /// /// The logger. /// logger protected BasePeriodicWebSocketListener(ILogger logger) { if (logger == null) { throw new ArgumentNullException("logger"); } Logger = logger; } /// /// The null task result /// protected Task NullTaskResult = Task.FromResult(true); /// /// Processes the message. /// /// The message. /// Task. public Task ProcessMessage(WebSocketMessageInfo message) { if (message.MessageType.Equals(Name + "Start", StringComparison.OrdinalIgnoreCase)) { Start(message); } if (message.MessageType.Equals(Name + "Stop", StringComparison.OrdinalIgnoreCase)) { Stop(message); } return NullTaskResult; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Starts sending messages over a web socket /// /// The message. private void Start(WebSocketMessageInfo message) { var vals = message.Data.Split(','); var dueTimeMs = long.Parse(vals[0], UsCulture); var periodMs = long.Parse(vals[1], UsCulture); var cancellationTokenSource = new CancellationTokenSource(); Logger.Info("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); var timer = new Timer(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite); var state = new TStateType(); var semaphore = new SemaphoreSlim(1, 1); lock (ActiveConnections) { ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, timer, state, semaphore)); } timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); } /// /// Timers the callback. /// /// The state. private async void TimerCallback(object state) { var connection = (IWebSocketConnection)state; Tuple tuple; lock (ActiveConnections) { tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); } if (tuple == null) { return; } if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) { DisposeConnection(tuple); return; } try { await tuple.Item5.WaitAsync(tuple.Item2.Token).ConfigureAwait(false); var data = await GetDataToSend(tuple.Item4).ConfigureAwait(false); if (data != null) { await connection.SendAsync(new WebSocketMessage { MessageType = Name, Data = data }, tuple.Item2.Token).ConfigureAwait(false); } tuple.Item5.Release(); } catch (OperationCanceledException) { if (tuple.Item2.IsCancellationRequested) { DisposeConnection(tuple); } } catch (Exception ex) { Logger.ErrorException("Error sending web socket message {0}", ex, Name); DisposeConnection(tuple); } } /// /// Stops sending messages over a web socket /// /// The message. private void Stop(WebSocketMessageInfo message) { lock (ActiveConnections) { var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); if (connection != null) { DisposeConnection(connection); } } } /// /// Disposes the connection. /// /// The connection. private void DisposeConnection(Tuple connection) { Logger.Info("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); try { connection.Item3.Dispose(); } catch (ObjectDisposedException) { } try { connection.Item2.Cancel(); connection.Item2.Dispose(); } catch (ObjectDisposedException) { } try { connection.Item5.Dispose(); } catch (ObjectDisposedException) { } ActiveConnections.Remove(connection); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { lock (ActiveConnections) { foreach (var connection in ActiveConnections.ToList()) { DisposeConnection(connection); } } } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); } } }