diff --git a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs index d3d69ea..b40e9aa 100644 --- a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs +++ b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs @@ -6,11 +6,11 @@ using ArmaForces.Arma.Server.Tests.Helpers; using ArmaForces.ArmaServerManager.Extensions; using ArmaForces.ArmaServerManager.Features.Mods; -using ArmaForces.ArmaServerManager.Features.Steam; using ArmaForces.ArmaServerManager.Features.Steam.Content; using ArmaForces.ArmaServerManager.Features.Steam.Content.DTOs; using AutoFixture; using CSharpFunctionalExtensions; +using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -24,7 +24,6 @@ public class ModsManagerUnitTests private readonly Mock _modsCacheMock; private readonly Mock _contentVerifierMock; private readonly Mock _downloaderMock; - private readonly Mock _steamClientMock; private readonly ModsManager _modsManager; public ModsManagerUnitTests() @@ -32,12 +31,10 @@ public ModsManagerUnitTests() _modsCacheMock = CreateModsCacheMock(); _contentVerifierMock = CreateContentVerifierMock(); _downloaderMock = CreateContentDownloaderMock(); - _steamClientMock = CreateSteamClientMock(); _modsManager = new ModsManager( _downloaderMock.Object, _contentVerifierMock.Object, _modsCacheMock.Object, - _steamClientMock.Object, new NullLogger()); } @@ -206,16 +203,5 @@ private Mock CreateContentDownloaderMock() return mock; } - - private Mock CreateSteamClientMock() - { - var mock = new Mock(); - - mock - .Setup(x => x.EnsureConnected(It.IsAny())) - .Returns(Task.CompletedTask); - - return mock; - } } } \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/ArmaForces.ArmaServerManager.csproj b/ArmaForces.ArmaServerManager/ArmaForces.ArmaServerManager.csproj index e8391bc..7c04301 100644 --- a/ArmaForces.ArmaServerManager/ArmaForces.ArmaServerManager.csproj +++ b/ArmaForces.ArmaServerManager/ArmaForces.ArmaServerManager.csproj @@ -17,6 +17,7 @@ + diff --git a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs index 48cb7f7..d05eefb 100644 --- a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs +++ b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs @@ -7,7 +7,6 @@ using ArmaForces.Arma.Server.Features.Mods; using ArmaForces.Arma.Server.Features.Modsets; using ArmaForces.ArmaServerManager.Extensions; -using ArmaForces.ArmaServerManager.Features.Steam; using ArmaForces.ArmaServerManager.Features.Steam.Content; using CSharpFunctionalExtensions; using Microsoft.Extensions.Logging; @@ -20,26 +19,22 @@ internal class ModsManager : IModsManager private readonly IContentDownloader _contentDownloader; private readonly IContentVerifier _contentVerifier; private readonly IModsCache _modsCache; - private readonly ISteamClient _steamClient; private readonly ILogger _logger; /// /// Client for mods download and updating. /// Client for verifying whether mods are up to date and correct. /// Installed mods cache. - /// /// Logger. public ModsManager( IContentDownloader contentDownloader, IContentVerifier contentVerifier, IModsCache modsCache, - ISteamClient steamClient, ILogger logger) { _contentDownloader = contentDownloader; _contentVerifier = contentVerifier; _modsCache = modsCache; - _steamClient = steamClient; _logger = logger; } @@ -96,8 +91,6 @@ await _contentVerifier.ItemIsUpToDate(mod.AsContentItem(), cancellationToken) /// used for mods download safe cancelling. private async Task CheckUpdatesAndDownloadMods(IEnumerable modsToDownload, CancellationToken cancellationToken) { - await _steamClient.EnsureConnected(cancellationToken); - return await CheckModsUpdated(modsToDownload.ToList(), cancellationToken) .Bind(modsMissingOrOutdated => DownloadMods(modsMissingOrOutdated, cancellationToken)); } diff --git a/ArmaForces.ArmaServerManager/Features/Steam/Content/ContentDownloader.cs b/ArmaForces.ArmaServerManager/Features/Steam/Content/ContentDownloader.cs index 05f52c1..bf6a56f 100644 --- a/ArmaForces.ArmaServerManager/Features/Steam/Content/ContentDownloader.cs +++ b/ArmaForces.ArmaServerManager/Features/Steam/Content/ContentDownloader.cs @@ -27,12 +27,17 @@ public class ContentDownloader : IContentDownloader private readonly ISteamClient _steamClient; private readonly ILogger _logger; + /// + /// Application settings. + /// Client used for connection. + /// Logger + // Used by DI + // ReSharper disable once UnusedMember.Global public ContentDownloader( ISettings settings, ISteamClient steamClient, - ILogger contentDownloader) - : this(steamClient, settings.ModsDirectory!, - contentDownloader) + ILogger logger) + : this(steamClient, settings.ModsDirectory!, logger) { } @@ -50,6 +55,12 @@ private ContentDownloader( _modsDirectory = modsDirectory; } + /// + /// Downloads or updates given collection. + /// + /// Collection of mods to update. + /// + /// public async Task>> DownloadOrUpdateMods( IReadOnlyCollection mods, CancellationToken cancellationToken) diff --git a/ArmaForces.ArmaServerManager/Features/Steam/Content/ManifestDownloader.cs b/ArmaForces.ArmaServerManager/Features/Steam/Content/ManifestDownloader.cs index ae43f4a..39d7607 100644 --- a/ArmaForces.ArmaServerManager/Features/Steam/Content/ManifestDownloader.cs +++ b/ArmaForces.ArmaServerManager/Features/Steam/Content/ManifestDownloader.cs @@ -6,6 +6,7 @@ using BytexDigital.Steam.ContentDelivery.Models; using BytexDigital.Steam.Core.Structs; using Microsoft.Extensions.Logging; +using Polly; using SteamKit2; namespace ArmaForces.ArmaServerManager.Features.Steam.Content @@ -22,73 +23,55 @@ public ManifestDownloader(ISteamClient steamClient, ILogger } public async Task GetManifest(ContentItem contentItem, CancellationToken cancellationToken) - => await _steamClient.ContentClient.GetManifestAsync( + { + await _steamClient.EnsureConnected(cancellationToken); + + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(15)); + + return await _steamClient.ContentClient.GetManifestAsync( appId: SteamConstants.ArmaAppId, depotId: SteamConstants.ArmaWorkshopDepotId, - manifestId: await GetManifestId(contentItem, cancellationToken), - cancellationToken: cancellationToken); - - /// - /// TODO: Do it better - /// + manifestId: await GetManifestId(contentItem, cancellationTokenSource.Token), + cancellationToken: cancellationTokenSource.Token); + } + private async Task GetManifestId(ContentItem contentItem, CancellationToken cancellationToken) { - _logger.LogDebug("Downloading ManifestId for item {ContentItemId}", contentItem.Id); + var asyncJobFailedPolicy = Policy + .Handle() + .WaitAndRetryAsync( + retryCount: SteamContentConstants.MaximumRetryCount, + sleepDurationProvider: _ => TimeSpan.FromSeconds(5), + onRetry: (result, _, _) => LogManifestIdDownloadFailure(result.Exception, contentItem)); - var errors = 0; + var taskCanceledPolicy = Policy + .Handle() + .FallbackAsync(Task.FromCanceled); - // TODO: Use Polly - while (true) - { - try - { - return (await _steamClient.ContentClient.GetPublishedFileDetailsAsync(contentItem.Id, cancellationToken)) - .hcontent_file; - } - catch (TaskCanceledException exception) - { - errors++; - LogManifestIdDownloadFailure(contentItem, exception, errors); - - if (errors >= SteamContentConstants.MaximumRetryCount) - { - throw CreateManifestDownloadException(errors, contentItem, exception); - } - - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - } - catch (AsyncJobFailedException exception) - { - errors++; - LogManifestIdDownloadFailure(contentItem, exception, errors); + var policy = Policy.WrapAsync(asyncJobFailedPolicy, taskCanceledPolicy); - if (errors >= SteamContentConstants.MaximumRetryCount) - { - throw CreateManifestDownloadException(errors, contentItem, exception); - } + _logger.LogDebug("Downloading ManifestId for item {ContentItemId}", contentItem.Id); + + var result = await policy.ExecuteAndCaptureAsync(async token => + (await _steamClient.ContentClient.GetPublishedFileDetailsAsync(contentItem.Id, token)).hcontent_file, + cancellationToken); - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - } - } + return result.Outcome == OutcomeType.Successful + ? result.Result + : throw CreateManifestDownloadException(contentItem, result.FinalException); } - private void LogManifestIdDownloadFailure( - ContentItem contentItem, - Exception exception, - int errors) + private void LogManifestIdDownloadFailure(Exception exception, ContentItem contentItem) => _logger.LogTrace( exception, - "Failed to download ManifestId for item {ContentItemId}. Errors = {Number}", - contentItem.Id, - errors); + "Failed to download ManifestId for item {ContentItemId}", + contentItem.Id); - private Exception CreateManifestDownloadException( - int errors, - ContentItem contentItem, - Exception? innerException = null) + private Exception CreateManifestDownloadException(ContentItem contentItem, Exception? innerException = null) { var newException = new Exception( - $"{errors} errors while attempting to download manifest for {contentItem.Id}", + $"Failed while attempting to download manifest for {contentItem.Id}", innerException); if (innerException is null) @@ -101,10 +84,10 @@ private Exception CreateManifestDownloadException( else { _logger.LogError( - newException, + innerException, "Could not download ManifestId for item {ContentItemId}, error message {Message}", contentItem.Id, - innerException.Message); + newException.Message); } return newException; diff --git a/ArmaForces.ArmaServerManager/Features/Steam/SteamClient.cs b/ArmaForces.ArmaServerManager/Features/Steam/SteamClient.cs index 84e3864..efa8ab9 100644 --- a/ArmaForces.ArmaServerManager/Features/Steam/SteamClient.cs +++ b/ArmaForces.ArmaServerManager/Features/Steam/SteamClient.cs @@ -15,6 +15,9 @@ internal class SteamClient : ISteamClient, IDisposable { private readonly ILogger _logger; private readonly BytexSteamClient _bytexSteamClient; + private readonly Guid _clientGuid = Guid.NewGuid(); + + private bool _isConnected; /// /// Settings containing steam user, password and mods directory. @@ -40,7 +43,9 @@ public SteamClient( /// public async Task EnsureConnected(CancellationToken cancellationToken) { - _logger.LogDebug("Ensuring connected to Steam"); + if (_isConnected) return; + + _logger.LogDebug("Ensuring connected to Steam with client {Guid}", _clientGuid); var connectCancellationTokenSource = new CancellationTokenSource(); var connectTask = _bytexSteamClient.ConnectAsync(connectCancellationTokenSource.Token); var connectionTimeout = Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); @@ -59,16 +64,17 @@ public async Task EnsureConnected(CancellationToken cancellationToken) _logger.LogError("Invalid Steam credentials"); throw new InvalidCredentialException("Invalid Steam Credentials"); } + + _isConnected = true; } - // TODO: Consider 'using' when operating on SteamClient, probably limit it to job scope public void Dispose() => Disconnect(); - - /// - public void Disconnect() + + private void Disconnect() { - _logger.LogDebug("Disconnecting from Steam"); + _logger.LogInformation("Disconnecting client {Guid} from Steam", _clientGuid); _bytexSteamClient.Shutdown(); + _isConnected = false; } } }