diff --git a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs index f2b8e7a8f..42f770905 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs @@ -38,6 +38,7 @@ using ArchiSteamFarm.NLog; using ArchiSteamFarm.NLog.Targets; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -45,6 +46,14 @@ namespace ArchiSteamFarm.IPC.Controllers.Api; public sealed class NLogController : ArchiController { private static readonly ConcurrentDictionary ActiveLogWebSockets = new(); + private readonly IHostApplicationLifetime ApplicationLifetime; + + public NLogController(IHostApplicationLifetime applicationLifetime) { + ArgumentNullException.ThrowIfNull(applicationLifetime); + + ApplicationLifetime = applicationLifetime; + } + /// /// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration. /// @@ -91,7 +100,7 @@ public sealed class NLogController : ArchiController { [HttpGet] [ProducesResponseType>>((int) HttpStatusCode.OK)] [ProducesResponseType((int) HttpStatusCode.BadRequest)] - public async Task Get(CancellationToken cancellationToken) { + public async Task Get() { if (HttpContext == null) { throw new InvalidOperationException(nameof(HttpContext)); } @@ -107,7 +116,9 @@ public sealed class NLogController : ArchiController { SemaphoreSlim sendSemaphore = new(1, 1); - if (!ActiveLogWebSockets.TryAdd(webSocket, (sendSemaphore, cancellationToken))) { + using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted, ApplicationLifetime.ApplicationStopping); + + if (!ActiveLogWebSockets.TryAdd(webSocket, (sendSemaphore, cancellationTokenSource.Token))) { sendSemaphore.Dispose(); return new EmptyResult(); @@ -116,20 +127,22 @@ public sealed class NLogController : ArchiController { try { // Push initial history if available if (ArchiKestrel.HistoryTarget != null) { - // ReSharper disable once AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block - await Task.WhenAll(ArchiKestrel.HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocket, archivedMessage, sendSemaphore, cancellationToken))).ConfigureAwait(false); + // ReSharper disable AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block + await Task.WhenAll(ArchiKestrel.HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocket, archivedMessage, sendSemaphore, cancellationTokenSource.Token))).ConfigureAwait(false); + + // ReSharper restore AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block } - while (webSocket.State == WebSocketState.Open) { - WebSocketReceiveResult result = await webSocket.ReceiveAsync(Array.Empty(), cancellationToken).ConfigureAwait(false); + while ((webSocket.State == WebSocketState.Open) && !cancellationTokenSource.IsCancellationRequested) { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(Array.Empty(), cancellationTokenSource.Token).ConfigureAwait(false); if (result.MessageType != WebSocketMessageType.Close) { - await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", cancellationToken).ConfigureAwait(false); + await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", cancellationTokenSource.Token).ConfigureAwait(false); break; } - await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", cancellationTokenSource.Token).ConfigureAwait(false); break; }