Skip to content

Commit

Permalink
Prevent attempts to connect to Steam multiple times
Browse files Browse the repository at this point in the history
  • Loading branch information
3Mydlo3 committed Aug 26, 2023
1 parent 1509ef0 commit 5047773
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,20 +24,17 @@ public class ModsManagerUnitTests
private readonly Mock<IModsCache> _modsCacheMock;
private readonly Mock<IContentVerifier> _contentVerifierMock;
private readonly Mock<IContentDownloader> _downloaderMock;
private readonly Mock<ISteamClient> _steamClientMock;
private readonly ModsManager _modsManager;

public ModsManagerUnitTests()
{
_modsCacheMock = CreateModsCacheMock();
_contentVerifierMock = CreateContentVerifierMock();
_downloaderMock = CreateContentDownloaderMock();
_steamClientMock = CreateSteamClientMock();
_modsManager = new ModsManager(
_downloaderMock.Object,
_contentVerifierMock.Object,
_modsCacheMock.Object,
_steamClientMock.Object,
new NullLogger<ModsManager>());
}

Expand Down Expand Up @@ -206,16 +203,5 @@ private Mock<IContentDownloader> CreateContentDownloaderMock()

return mock;
}

private Mock<ISteamClient> CreateSteamClientMock()
{
var mock = new Mock<ISteamClient>();

mock
.Setup(x => x.EnsureConnected(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);

return mock;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageReference Include="Hangfire.LiteDB" Version="0.4.1" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Polly" Version="6.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.3.1" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
</ItemGroup>
Expand Down
7 changes: 0 additions & 7 deletions ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ModsManager> _logger;

/// <inheritdoc cref="ModsManager" />
/// <param name="contentDownloader">Client for mods download and updating.</param>
/// <param name="contentVerifier">Client for verifying whether mods are up to date and correct.</param>
/// <param name="modsCache">Installed mods cache.</param>
/// <param name="steamClient"></param>
/// <param name="logger">Logger.</param>
public ModsManager(
IContentDownloader contentDownloader,
IContentVerifier contentVerifier,
IModsCache modsCache,
ISteamClient steamClient,
ILogger<ModsManager> logger)
{
_contentDownloader = contentDownloader;
_contentVerifier = contentVerifier;
_modsCache = modsCache;
_steamClient = steamClient;
_logger = logger;
}

Expand Down Expand Up @@ -96,8 +91,6 @@ await _contentVerifier.ItemIsUpToDate(mod.AsContentItem(), cancellationToken)
/// <param name="cancellationToken"><see cref="CancellationToken" /> used for mods download safe cancelling.</param>
private async Task<Result> CheckUpdatesAndDownloadMods(IEnumerable<Mod> modsToDownload, CancellationToken cancellationToken)
{
await _steamClient.EnsureConnected(cancellationToken);

return await CheckModsUpdated(modsToDownload.ToList(), cancellationToken)
.Bind(modsMissingOrOutdated => DownloadMods(modsMissingOrOutdated, cancellationToken));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ public class ContentDownloader : IContentDownloader
private readonly ISteamClient _steamClient;
private readonly ILogger<ContentDownloader> _logger;

/// <inheritdoc />
/// <param name="settings">Application settings.</param>
/// <param name="steamClient">Client used for connection.</param>
/// <param name="logger">Logger</param>
// Used by DI
// ReSharper disable once UnusedMember.Global
public ContentDownloader(
ISettings settings,
ISteamClient steamClient,
ILogger<ContentDownloader> contentDownloader)
: this(steamClient, settings.ModsDirectory!,
contentDownloader)
ILogger<ContentDownloader> logger)
: this(steamClient, settings.ModsDirectory!, logger)
{
}

Expand All @@ -50,6 +55,12 @@ private ContentDownloader(
_modsDirectory = modsDirectory;
}

/// <summary>
/// Downloads or updates given <paramref name="mods"/> collection.
/// </summary>
/// <param name="mods">Collection of mods to update.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<List<Result<Mod>>> DownloadOrUpdateMods(
IReadOnlyCollection<Mod> mods,
CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,73 +23,55 @@ public ManifestDownloader(ISteamClient steamClient, ILogger<ManifestDownloader>
}

public async Task<Manifest> 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);

/// <summary>
/// TODO: Do it better
/// </summary>
manifestId: await GetManifestId(contentItem, cancellationTokenSource.Token),
cancellationToken: cancellationTokenSource.Token);
}

private async Task<ManifestId> GetManifestId(ContentItem contentItem, CancellationToken cancellationToken)
{
_logger.LogDebug("Downloading ManifestId for item {ContentItemId}", contentItem.Id);
var asyncJobFailedPolicy = Policy<ulong>
.Handle<AsyncJobFailedException>()
.WaitAndRetryAsync(
retryCount: SteamContentConstants.MaximumRetryCount,
sleepDurationProvider: _ => TimeSpan.FromSeconds(5),
onRetry: (result, _, _) => LogManifestIdDownloadFailure(result.Exception, contentItem));

var errors = 0;
var taskCanceledPolicy = Policy<ulong>
.Handle<TaskCanceledException>()
.FallbackAsync(Task.FromCanceled<ulong>);

// 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)
Expand All @@ -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;
Expand Down
18 changes: 12 additions & 6 deletions ArmaForces.ArmaServerManager/Features/Steam/SteamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal class SteamClient : ISteamClient, IDisposable
{
private readonly ILogger<SteamClient> _logger;
private readonly BytexSteamClient _bytexSteamClient;
private readonly Guid _clientGuid = Guid.NewGuid();

private bool _isConnected;

/// <inheritdoc cref="ISteamClient" />
/// <param name="settings">Settings containing steam user, password and mods directory.</param>
Expand All @@ -40,7 +43,9 @@ public SteamClient(
/// </exception>
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);
Expand All @@ -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();

/// <inheritdoc />
public void Disconnect()

private void Disconnect()
{
_logger.LogDebug("Disconnecting from Steam");
_logger.LogInformation("Disconnecting client {Guid} from Steam", _clientGuid);
_bytexSteamClient.Shutdown();
_isConnected = false;
}
}
}

0 comments on commit 5047773

Please sign in to comment.