diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 5169f65adb..e27a8975b7 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -889,12 +889,6 @@ namespace Emby.Dlna.PlayTo return Task.CompletedTask; } - /// - public Task CloseAllWebSockets(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - private class StreamParams { private MediaSourceInfo _mediaSource; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 82294644b8..4f74136337 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -111,7 +111,7 @@ namespace Emby.Server.Implementations /// /// Class CompositionRoot. /// - public abstract class ApplicationHost : IServerApplicationHost, IDisposable + public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable { /// /// The environment variable prefixes to log at server startup. @@ -1230,5 +1230,24 @@ namespace Emby.Server.Implementations _disposed = true; } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Used to perform asynchronous cleanup of managed resources or for cascading calls to . + /// + /// A ValueTask. + protected virtual async ValueTask DisposeAsyncCore() + { + foreach (var session in _sessionManager.Sessions) + { + await session.DisposeAsync().ConfigureAwait(false); + } + } } } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 331b5aa37a..74d9e91450 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// Class WebSocketConnection. /// - public class WebSocketConnection : IWebSocketConnection, IDisposable + public class WebSocketConnection : IWebSocketConnection, IAsyncDisposable, IDisposable { /// /// The logger. @@ -231,12 +231,6 @@ namespace Emby.Server.Implementations.HttpServer CancellationToken.None); } - /// - public async Task CloseSocket(CancellationToken cancellationToken) - { - await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", cancellationToken).ConfigureAwait(false); - } - /// public void Dispose() { @@ -255,5 +249,25 @@ namespace Emby.Server.Implementations.HttpServer _socket.Dispose(); } } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + Dispose(false); + GC.SuppressFinalize(this); + } + + /// + /// Used to perform asynchronous cleanup of managed resources or for cascading calls to . + /// + /// A ValueTask. + protected virtual async ValueTask DisposeAsyncCore() + { + if (_socket.State == WebSocketState.Open) + { + await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false); + } + } } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 9c00e9e9f4..278e71ef3c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1363,26 +1363,9 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - await CloseAllWebSockets(cancellationToken).ConfigureAwait(false); - await SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken).ConfigureAwait(false); } - /// - /// Gracefully closes all web sockets in all sessions. - /// - /// The cancellation token. - private async Task CloseAllWebSockets(CancellationToken cancellationToken) - { - foreach (var session in Sessions) - { - foreach (var sessionController in session.SessionControllers) - { - await sessionController.CloseAllWebSockets(cancellationToken).ConfigureAwait(false); - } - } - } - /// /// Sends the server restart notification. /// diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index f9757a0af5..373f04f625 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session { - public sealed class WebSocketController : ISessionController, IDisposable + public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable { private readonly ILogger _logger; private readonly ISessionManager _sessionManager; @@ -88,15 +88,6 @@ namespace Emby.Server.Implementations.Session cancellationToken); } - /// - public async Task CloseAllWebSockets(CancellationToken cancellationToken) - { - foreach (var socket in _sockets) - { - await socket.CloseSocket(cancellationToken).ConfigureAwait(false); - } - } - /// public void Dispose() { @@ -112,5 +103,25 @@ namespace Emby.Server.Implementations.Session _disposed = true; } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + foreach (var socket in _sockets) + { + socket.Closed -= OnConnectionClosed; + + if (socket is IAsyncDisposable disposableAsync) + { + await disposableAsync.DisposeAsync().ConfigureAwait(false); + } + } + + _disposed = true; + } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 1323d8a480..40685cae84 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -243,7 +243,7 @@ namespace Jellyfin.Server } } - appHost.Dispose(); + await appHost.DisposeAsync().ConfigureAwait(false); } if (_restartOnShutdown) diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index cea2049fd7..2c6483ae28 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -58,12 +58,5 @@ namespace MediaBrowser.Controller.Net Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken); Task ProcessAsync(CancellationToken cancellationToken = default); - - /// - /// Gracefully closes the socket. - /// - /// Task. - /// The cancellation token. - Task CloseSocket(CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index 3acfe19a27..b38ee11462 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -33,12 +33,5 @@ namespace MediaBrowser.Controller.Session /// CancellationToken for operation. /// A task. Task SendMessage(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken); - - /// - /// Gracefully closes all web sockets. - /// - /// The cancellation token. - /// A task. - Task CloseAllWebSockets(CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index c2ca233868..7a2993a10e 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Session /// /// Class SessionInfo. /// - public sealed class SessionInfo : IDisposable + public sealed class SessionInfo : IAsyncDisposable, IDisposable { // 1 second private const long ProgressIncrement = 10000000; @@ -380,10 +381,28 @@ namespace MediaBrowser.Controller.Session { if (controller is IDisposable disposable) { - _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); + _logger.LogInformation("Disposing session controller synchronously {0}", disposable.GetType().Name); disposable.Dispose(); } } } + + public async ValueTask DisposeAsync() + { + _disposed = true; + + StopAutomaticProgress(); + + var controllers = SessionControllers.ToList(); + + foreach (var controller in controllers) + { + if (controller is IAsyncDisposable disposableAsync) + { + _logger.LogInformation("Disposing session controller asynchronously {0}", disposableAsync.GetType().Name); + await disposableAsync.DisposeAsync().ConfigureAwait(false); + } + } + } } }