From a4263efefefb89cd91add4c3b6227787dcfd711f Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Wed, 15 Nov 2023 22:34:11 +0100 Subject: [PATCH 1/5] Implement quick mods update to run before server startup --- .../Features/Mods/ModsManagerUnitTests.cs | 15 +++ .../Api/Mods/ModsController.cs | 19 +++- .../Api/Status/DTOs/AppStatus.cs | 5 + .../ModsServiceCollectionExtensions.cs | 2 + .../Features/Mods/IModsManager.cs | 15 ++- .../Features/Mods/ModsManager.cs | 34 +++++++ .../Features/Status/AppStatusStore.cs | 57 +++++++++++ .../Features/Status/StatusProvider.cs | 19 +++- .../RemoteStorage/ISteamRemoteStorage.cs | 98 +++++++++++++++++++ .../Properties/launchSettings.json | 2 + .../Services/IModsUpdateService.cs | 5 +- .../Services/IModsVerificationService.cs | 29 ++++++ .../Services/ModsVerificationService.cs | 17 +++- .../Services/ServerStartupService.cs | 12 ++- ArmaForces.ArmaServerManager/Startup.cs | 2 + 15 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 ArmaForces.ArmaServerManager/Features/Status/AppStatusStore.cs create mode 100644 ArmaForces.ArmaServerManager/Features/Steam/RemoteStorage/ISteamRemoteStorage.cs create mode 100644 ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs diff --git a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs index b40e9aa..49279ea 100644 --- a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs +++ b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs @@ -8,6 +8,7 @@ using ArmaForces.ArmaServerManager.Features.Mods; using ArmaForces.ArmaServerManager.Features.Steam.Content; using ArmaForces.ArmaServerManager.Features.Steam.Content.DTOs; +using ArmaForces.ArmaServerManager.Features.Steam.RemoteStorage; using AutoFixture; using CSharpFunctionalExtensions; using FluentAssertions; @@ -24,6 +25,7 @@ public class ModsManagerUnitTests private readonly Mock _modsCacheMock; private readonly Mock _contentVerifierMock; private readonly Mock _downloaderMock; + private readonly Mock _steamRemoteStorageMock; private readonly ModsManager _modsManager; public ModsManagerUnitTests() @@ -31,10 +33,12 @@ public ModsManagerUnitTests() _modsCacheMock = CreateModsCacheMock(); _contentVerifierMock = CreateContentVerifierMock(); _downloaderMock = CreateContentDownloaderMock(); + _steamRemoteStorageMock = CreateSteamRemoteStorageMock(); _modsManager = new ModsManager( _downloaderMock.Object, _contentVerifierMock.Object, _modsCacheMock.Object, + _steamRemoteStorageMock.Object, new NullLogger()); } @@ -203,5 +207,16 @@ private Mock CreateContentDownloaderMock() return mock; } + + private Mock CreateSteamRemoteStorageMock() + { + var mock = new Mock(); + + mock + .Setup(x => x.GetPublishedFileDetails(It.IsAny>(), It.IsAny())) + .Returns(Task.FromResult(new Result())); + + return mock; + } } } \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Api/Mods/ModsController.cs b/ArmaForces.ArmaServerManager/Api/Mods/ModsController.cs index e937f00..65da252 100644 --- a/ArmaForces.ArmaServerManager/Api/Mods/ModsController.cs +++ b/ArmaForces.ArmaServerManager/Api/Mods/ModsController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using ArmaForces.ArmaServerManager.Api.Jobs.DTOs; using ArmaForces.ArmaServerManager.Api.Mods.DTOs; @@ -93,14 +93,25 @@ public IActionResult UpdateModset(string modsetName, [FromBody] ModsetUpdateRequ } /// Verify Modset - /// Triggers or schedules verification of given modset. Not implemented. + /// Triggers or schedules verification of given modset. /// Name of modset to verify. /// Optional job schedule details. [HttpPost("{modsetName}/verify", Name = nameof(VerifyModset))] - [ProducesResponseType(StatusCodes.Status501NotImplemented)] + [ProducesResponseType(typeof(int), StatusCodes.Status202Accepted)] + [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] public IActionResult VerifyModset(string modsetName, [FromBody] JobScheduleRequestDto jobScheduleRequestDto) { - throw new NotImplementedException("Modset verification is not implemented yet."); + var result = _jobsScheduler + .ScheduleJob( + x => x.ShutdownAllServers(false, CancellationToken.None), + jobScheduleRequestDto.ScheduleAt) + .Bind(shutdownJobId => _jobsScheduler.ContinueJobWith( + shutdownJobId, + x => x.VerifyModset(modsetName, CancellationToken.None))); + + return result.Match( + onSuccess: JobAccepted, + onFailure: TooEarly); } } } diff --git a/ArmaForces.ArmaServerManager/Api/Status/DTOs/AppStatus.cs b/ArmaForces.ArmaServerManager/Api/Status/DTOs/AppStatus.cs index fad4ed8..fa8bc5d 100644 --- a/ArmaForces.ArmaServerManager/Api/Status/DTOs/AppStatus.cs +++ b/ArmaForces.ArmaServerManager/Api/Status/DTOs/AppStatus.cs @@ -16,6 +16,11 @@ public enum AppStatus /// UpdatingMods, + /// + /// Mods are being verified. + /// + VerifyingMods, + /// /// Server is starting. /// diff --git a/ArmaForces.ArmaServerManager/Features/Mods/DependencyInjection/ModsServiceCollectionExtensions.cs b/ArmaForces.ArmaServerManager/Features/Mods/DependencyInjection/ModsServiceCollectionExtensions.cs index 3c3f88e..61f1acb 100644 --- a/ArmaForces.ArmaServerManager/Features/Mods/DependencyInjection/ModsServiceCollectionExtensions.cs +++ b/ArmaForces.ArmaServerManager/Features/Mods/DependencyInjection/ModsServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using ArmaForces.ArmaServerManager.Features.Modsets.DependencyInjection; using ArmaForces.ArmaServerManager.Features.Steam; using ArmaForces.ArmaServerManager.Features.Steam.Content; +using ArmaForces.ArmaServerManager.Features.Steam.RemoteStorage; using Microsoft.Extensions.DependencyInjection; namespace ArmaForces.ArmaServerManager.Features.Mods.DependencyInjection @@ -23,6 +24,7 @@ public static IServiceCollection AddMods(this IServiceCollection services) private static IServiceCollection AddContent(this IServiceCollection services) => services .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/ArmaForces.ArmaServerManager/Features/Mods/IModsManager.cs b/ArmaForces.ArmaServerManager/Features/Mods/IModsManager.cs index 261237d..f945972 100644 --- a/ArmaForces.ArmaServerManager/Features/Mods/IModsManager.cs +++ b/ArmaForces.ArmaServerManager/Features/Mods/IModsManager.cs @@ -5,11 +5,13 @@ using ArmaForces.Arma.Server.Features.Modsets; using CSharpFunctionalExtensions; -namespace ArmaForces.ArmaServerManager.Features.Mods { +namespace ArmaForces.ArmaServerManager.Features.Mods +{ /// /// Prepares modset by downloading missing mods and updating outdated mods. /// - public interface IModsManager { + public interface IModsManager + { /// /// Modset to prepare. /// @@ -27,6 +29,7 @@ public interface IModsManager { /// Checks if all mods from given list are up to date. /// /// List of mods to check. + /// Cancellation token. /// with outdated mods. Task>> CheckModsUpdated(IReadOnlyCollection modsList, CancellationToken cancellationToken); @@ -42,5 +45,13 @@ public interface IModsManager { /// /// used for task cancellation. Task UpdateAllMods(CancellationToken cancellationToken); + + /// + /// Verifies all mods from given . + /// + /// List of mods to verify. + /// Cancellation Token + /// with mods which failed verification. + Task>> VerifyMods(IReadOnlyCollection modsList, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs index d05eefb..4fcb884 100644 --- a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs +++ b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -8,6 +9,8 @@ using ArmaForces.Arma.Server.Features.Modsets; using ArmaForces.ArmaServerManager.Extensions; using ArmaForces.ArmaServerManager.Features.Steam.Content; +using ArmaForces.ArmaServerManager.Features.Steam.Content.DTOs; +using ArmaForces.ArmaServerManager.Features.Steam.RemoteStorage; using CSharpFunctionalExtensions; using Microsoft.Extensions.Logging; @@ -19,22 +22,26 @@ internal class ModsManager : IModsManager private readonly IContentDownloader _contentDownloader; private readonly IContentVerifier _contentVerifier; private readonly IModsCache _modsCache; + private readonly ISteamRemoteStorage _steamRemoteStorage; private readonly ILogger _logger; /// /// Client for mods download and updating. /// Client for verifying whether mods are up to date and correct. /// Installed mods cache. + /// Steam remote storage for quick access to mods metadata on Workshop. /// Logger. public ModsManager( IContentDownloader contentDownloader, IContentVerifier contentVerifier, IModsCache modsCache, + ISteamRemoteStorage steamRemoteStorage, ILogger logger) { _contentDownloader = contentDownloader; _contentVerifier = contentVerifier; _modsCache = modsCache; + _steamRemoteStorage = steamRemoteStorage; _logger = logger; } @@ -65,6 +72,33 @@ public Result> CheckModsExist(IEnumerable modsList) /// public async Task>> CheckModsUpdated(IReadOnlyCollection modsList, CancellationToken cancellationToken) + { + if (modsList.IsEmpty()) + { + return Result.Success(new List()); + } + + var workshopModIds = modsList + .Where(x => x.Source == ModSource.SteamWorkshop) + .Select(x => x.WorkshopId) + .Where(x => x.HasValue) + .Select(x => (ulong)x!.Value) + .ToList(); + + var publishedFileDetails = await _steamRemoteStorage.GetPublishedFileDetails(workshopModIds, cancellationToken); + + var modsToUpdate = _modsCache.Mods + .Join(publishedFileDetails.Value, mod => mod.WorkshopId, fileDetails => fileDetails.PublishedFileId, + (mod, details) => new {mod, details}) + .Where(x => x.mod.LastUpdatedAt < x.details.LastUpdatedAt) + .Select(x => x.mod) + .ToList(); + + return Result.Success(modsToUpdate); + } + + /// + public async Task>> VerifyMods(IReadOnlyCollection modsList, CancellationToken cancellationToken) { if (modsList.IsEmpty()) { diff --git a/ArmaForces.ArmaServerManager/Features/Status/AppStatusStore.cs b/ArmaForces.ArmaServerManager/Features/Status/AppStatusStore.cs new file mode 100644 index 0000000..d9ee5be --- /dev/null +++ b/ArmaForces.ArmaServerManager/Features/Status/AppStatusStore.cs @@ -0,0 +1,57 @@ +using System; +using ArmaForces.ArmaServerManager.Api.Status.DTOs; +using ArmaForces.ArmaServerManager.Features.Status.Models; + +namespace ArmaForces.ArmaServerManager.Features.Status; + +/// +/// TODO: Consider doing this in a better way. For now it's fine. +/// +public class AppStatusStore : IAppStatusStore +{ + public AppStatusDetails? StatusDetails { get; private set; } + + public IDisposable SetAppStatus(AppStatus appStatus, string? longStatus = null) + { + var previousStatus = StatusDetails; + + StatusDetails = new AppStatusDetails + { + Status = appStatus, + LongStatus = longStatus + }; + + return new AppStatusDisposable(this, previousStatus); + } + + public void ClearAppStatus() + { + StatusDetails = null; + } + + private class AppStatusDisposable : IDisposable + { + private readonly AppStatusStore _appStatusStore; + private readonly AppStatusDetails? _previousStatus; + + public AppStatusDisposable(AppStatusStore appStatusStore, AppStatusDetails? previousStatus) + { + _appStatusStore = appStatusStore; + _previousStatus = previousStatus; + } + + public void Dispose() + { + _appStatusStore.StatusDetails = _previousStatus; + } + } +} + +public interface IAppStatusStore +{ + AppStatusDetails? StatusDetails { get; } + + IDisposable SetAppStatus(AppStatus appStatus, string? longStatus = null); + + void ClearAppStatus(); +} \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs b/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs index ded597e..69ba9ad 100644 --- a/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs +++ b/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs @@ -16,18 +16,23 @@ namespace ArmaForces.ArmaServerManager.Features.Status { internal class StatusProvider : IStatusProvider { + private readonly IAppStatusStore _appStatusStore; private readonly IJobsService _jobsService; private readonly IServerProvider _serverProvider; - public StatusProvider(IJobsService jobsService, IServerProvider serverProvider) + public StatusProvider( + IAppStatusStore appStatusStore, + IJobsService jobsService, + IServerProvider serverProvider) { + _appStatusStore = appStatusStore; _jobsService = jobsService; _serverProvider = serverProvider; } public async Task> GetAppStatus(IEnumerable include) { - var status = CreateSimpleAppStatus(GetCurrentJobDetails()); + var status = _appStatusStore.StatusDetails ?? CreateSimpleAppStatus(GetCurrentJobDetails()); var appStatusIncludesEnumerable = include as AppStatusIncludes[] ?? include.ToArray(); if (appStatusIncludesEnumerable.Contains(AppStatusIncludes.Jobs)) @@ -94,6 +99,16 @@ private static AppStatusDetails GetCurrentStatus(JobDetails? currentJobDetails) Status = AppStatus.UpdatingMods, LongStatus = $"Updating mods for {currentJobDetails.GetParameterValue("modsetName")}" }, + nameof(ModsVerificationService.VerifyModset) => new AppStatusDetails + { + Status = AppStatus.VerifyingMods, + LongStatus = $"Verifying mods from {currentJobDetails.GetParameterValue("modsetName")}" + }, + nameof(ModsVerificationService.VerifyMods) => new AppStatusDetails + { + Status = AppStatus.VerifyingMods, + LongStatus = $"Verifying mods with ids {currentJobDetails.GetParameterValue("modIds")}" + }, nameof(ServerStartupService.StartServerForMission) => new AppStatusDetails { Status = AppStatus.StartingServer, diff --git a/ArmaForces.ArmaServerManager/Features/Steam/RemoteStorage/ISteamRemoteStorage.cs b/ArmaForces.ArmaServerManager/Features/Steam/RemoteStorage/ISteamRemoteStorage.cs new file mode 100644 index 0000000..e228867 --- /dev/null +++ b/ArmaForces.ArmaServerManager/Features/Steam/RemoteStorage/ISteamRemoteStorage.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using CSharpFunctionalExtensions; + +namespace ArmaForces.ArmaServerManager.Features.Steam.RemoteStorage; + +/// +/// https://partner.steamgames.com/doc/webapi/ISteamRemoteStorage +/// +public interface ISteamRemoteStorage +{ + /// + /// Retrieves details of published file (workshop item). + /// + /// IDs of workshop items to retrieve details for. + /// Cancellation token. + Task> GetPublishedFileDetails(IEnumerable publishedFileIds, CancellationToken cancellationToken); +} + +internal class SteamRemoteStorage : ISteamRemoteStorage +{ + private const string GetPublishedFileDetailsUrl = + "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/"; + + private readonly HttpClient _httpClient; + + public SteamRemoteStorage(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> GetPublishedFileDetails(IEnumerable publishedFileIds, CancellationToken cancellationToken) + { + var idsQueryList = publishedFileIds.ToList(); + + var requestedItems = new Dictionary + { + {"itemcount", idsQueryList.Count.ToString()} + }; + + for (var i = 0; i < idsQueryList.Count; i++) + { + requestedItems.Add($"publishedfileids[{i}]", idsQueryList[i].ToString()); + } + + var requestContent = new FormUrlEncodedContent(requestedItems); + + var response = await _httpClient.PostAsync(GetPublishedFileDetailsUrl, requestContent, cancellationToken); + + var responseData = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) return Result.Failure(responseData); + + var publishedFileDetails = JsonSerializer.Deserialize(responseData)?.Single().Value?.AsObject() + ["publishedfiledetails"].Deserialize(); + + if (publishedFileDetails is null) return Result.Failure($"Failed parsing published file details. Response: {responseData}"); + + return Result.Success(publishedFileDetails + .Select(x => new PublishedFileDetails + { + PublishedFileId = long.Parse(x.PublishedFileId), + Title = x.Title, + LastUpdatedAt = DateTimeOffset.FromUnixTimeSeconds(x.TimeUpdated) + }) + .ToArray()); + } + + private record PublishedFileDetailsRaw + { + [JsonPropertyName("publishedfileid")] + public string PublishedFileId { get; init; } = string.Empty; + + [JsonPropertyName("title")] + public string Title { get; init; } = string.Empty; + + [JsonPropertyName("time_updated")] + public long TimeUpdated { get; init; } + } +} + +public record PublishedFileDetails +{ + public long PublishedFileId { get; init; } + + public string Title { get; init; } = string.Empty; + + public DateTimeOffset LastUpdatedAt { get; init; } +} \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Properties/launchSettings.json b/ArmaForces.ArmaServerManager/Properties/launchSettings.json index 737fb7e..92e1d4b 100644 --- a/ArmaForces.ArmaServerManager/Properties/launchSettings.json +++ b/ArmaForces.ArmaServerManager/Properties/launchSettings.json @@ -17,6 +17,8 @@ }, "Arma.Server.Manager": { "commandName": "Project", + "executablePath": "D:\\Program Files\\ArmaServerManager\\ArmaForces.ArmaServerManager.exe", + "workingDirectory": "D:\\Program Files\\ArmaServerManager", "launchBrowser": true, "applicationUrl": "https://localhost:5001;http://localhost:5000", "launchUrl": "api-docs/", diff --git a/ArmaForces.ArmaServerManager/Services/IModsUpdateService.cs b/ArmaForces.ArmaServerManager/Services/IModsUpdateService.cs index 2735dcb..14a0835 100644 --- a/ArmaForces.ArmaServerManager/Services/IModsUpdateService.cs +++ b/ArmaForces.ArmaServerManager/Services/IModsUpdateService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ArmaForces.Arma.Server.Features.Mods; @@ -7,6 +7,9 @@ namespace ArmaForces.ArmaServerManager.Services { + /// + /// Performs updates of installed mods. + /// public interface IModsUpdateService { Task UpdateModset(string modsetName, CancellationToken cancellationToken); diff --git a/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs b/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs new file mode 100644 index 0000000..0899a5d --- /dev/null +++ b/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ArmaForces.Arma.Server.Features.Modsets; +using CSharpFunctionalExtensions; + +namespace ArmaForces.ArmaServerManager.Services; + +/// +/// Performs detailed verifications of installed mods. +/// +public interface IModsVerificationService +{ + /// + /// Runs detailed verification of given . + /// + /// Modset with mods to verify. + /// Cancellation token. + /// Successful result if all mods were verified correctly. + Task VerifyModset(Modset modset, CancellationToken cancellationToken); + + /// + /// Retrieves modset with given and runs a detailed verification. + /// + /// Name of the modset to retrieve and verify. + /// Cancellation token. + /// Successful result if all mods were verified correctly. + Task VerifyModset(string modsetName, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs b/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs index 285791b..58c5fff 100644 --- a/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs +++ b/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using ArmaForces.Arma.Server.Features.Modsets; using ArmaForces.ArmaServerManager.Features.Mods; @@ -7,7 +9,10 @@ namespace ArmaForces.ArmaServerManager.Services { - public class ModsVerificationService + /// + /// Service allowing mods verification. + /// + public class ModsVerificationService : IModsVerificationService { private readonly IModsManager _modsManager; private readonly IModsetProvider _modsetProvider; @@ -18,13 +23,15 @@ public ModsVerificationService(IModsManager modsManager, IModsetProvider modsetP _modsetProvider = modsetProvider; } + /// public async Task VerifyModset(string modsetName, CancellationToken cancellationToken) => await _modsetProvider.GetModsetByName(modsetName) .Bind(x => VerifyModset(x, cancellationToken)); - private async Task VerifyModset(Modset modset, CancellationToken cancellationToken) + /// + public async Task VerifyModset(Modset modset, CancellationToken cancellationToken) { - return await _modsManager.PrepareModset(modset, cancellationToken); + return await _modsManager.VerifyMods(modset.Mods.ToList(), cancellationToken); } } -} +} \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Services/ServerStartupService.cs b/ArmaForces.ArmaServerManager/Services/ServerStartupService.cs index 8c98efa..dcc8d95 100644 --- a/ArmaForces.ArmaServerManager/Services/ServerStartupService.cs +++ b/ArmaForces.ArmaServerManager/Services/ServerStartupService.cs @@ -1,13 +1,16 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ArmaForces.Arma.Server.Features.Modsets; using ArmaForces.ArmaServerManager.Api.Servers.DTOs; +using ArmaForces.ArmaServerManager.Api.Status.DTOs; using ArmaForces.ArmaServerManager.Features.Missions; using ArmaForces.ArmaServerManager.Features.Missions.DTOs; using ArmaForces.ArmaServerManager.Features.Modsets; using ArmaForces.ArmaServerManager.Features.Servers; +using ArmaForces.ArmaServerManager.Features.Status; using CSharpFunctionalExtensions; using Microsoft.Extensions.Logging; @@ -20,6 +23,7 @@ public class ServerStartupService : IServerStartupService { private const int Port = 2302; + private readonly IAppStatusStore _appStatusStore; private readonly IApiMissionsClient _apiMissionsClient; private readonly IModsetProvider _modsetProvider; private readonly IServerCommandLogic _serverCommandLogic; @@ -27,12 +31,14 @@ public class ServerStartupService : IServerStartupService private readonly ILogger _logger; public ServerStartupService( + IAppStatusStore appStatusStore, IApiMissionsClient apiMissionsClient, IModsetProvider modsetProvider, IServerCommandLogic serverCommandLogic, IModsUpdateService modsUpdateService, ILogger logger) { + _appStatusStore = appStatusStore; _apiMissionsClient = apiMissionsClient; _modsetProvider = modsetProvider; _serverCommandLogic = serverCommandLogic; @@ -64,11 +70,15 @@ public async Task StartServer( public async Task StartServer(Modset modset, int headlessClients, CancellationToken cancellationToken) { + IDisposable? appStatusChanges = null; + return await ShutdownServer( Port, false, cancellationToken) - //.Bind(() => _modsUpdateService.UpdateModset(modset, cancellationToken)) + .Tap(() => appStatusChanges = _appStatusStore.SetAppStatus(AppStatus.UpdatingMods, $"Updating mods for server with '{modset.Name}' modset")) + .Bind(() => _modsUpdateService.UpdateModset(modset, cancellationToken)) + .Tap(() => appStatusChanges?.Dispose()) .Bind(() => _serverCommandLogic.StartServer(Port, headlessClients, modset)) .Tap(() => _logger.LogInformation("Successfully started server on {Port} port with {ModsetName} modset", Port, modset.Name)); } diff --git a/ArmaForces.ArmaServerManager/Startup.cs b/ArmaForces.ArmaServerManager/Startup.cs index a5b524c..8fd2ca1 100644 --- a/ArmaForces.ArmaServerManager/Startup.cs +++ b/ArmaForces.ArmaServerManager/Startup.cs @@ -101,6 +101,7 @@ public void ConfigureServices(IServiceCollection services) .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() // Arma Server @@ -124,6 +125,7 @@ public void ConfigureServices(IServiceCollection services) .AddSingleton() // Status + .AddSingleton() .AddSingleton() // Hangfire From 297424997c91a8e7ef05ccc4a55bc2be2ba3cb2d Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Wed, 15 Nov 2023 22:38:19 +0100 Subject: [PATCH 2/5] Fix incorrect media type error when making request without JSON body --- .../Api/Jobs/DTOs/JobScheduleRequestDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArmaForces.ArmaServerManager/Api/Jobs/DTOs/JobScheduleRequestDto.cs b/ArmaForces.ArmaServerManager/Api/Jobs/DTOs/JobScheduleRequestDto.cs index a0f9827..f063463 100644 --- a/ArmaForces.ArmaServerManager/Api/Jobs/DTOs/JobScheduleRequestDto.cs +++ b/ArmaForces.ArmaServerManager/Api/Jobs/DTOs/JobScheduleRequestDto.cs @@ -12,7 +12,7 @@ public class JobScheduleRequestDto /// Time when job should be processed. /// Exact start time will depend on other jobs processing and enqueued at this time. /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonProperty(Required = Required.DisallowNull, DefaultValueHandling = DefaultValueHandling.Ignore)] public DateTime? ScheduleAt { get; set; } } } From 5811752cd553f89fab7c04668f1b72957eb59b34 Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Wed, 15 Nov 2023 22:40:39 +0100 Subject: [PATCH 3/5] Remove verifyMods from status --- .../Features/Status/StatusProvider.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs b/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs index 69ba9ad..7b544c8 100644 --- a/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs +++ b/ArmaForces.ArmaServerManager/Features/Status/StatusProvider.cs @@ -104,11 +104,6 @@ private static AppStatusDetails GetCurrentStatus(JobDetails? currentJobDetails) Status = AppStatus.VerifyingMods, LongStatus = $"Verifying mods from {currentJobDetails.GetParameterValue("modsetName")}" }, - nameof(ModsVerificationService.VerifyMods) => new AppStatusDetails - { - Status = AppStatus.VerifyingMods, - LongStatus = $"Verifying mods with ids {currentJobDetails.GetParameterValue("modIds")}" - }, nameof(ServerStartupService.StartServerForMission) => new AppStatusDetails { Status = AppStatus.StartingServer, From f1766faacc662ffe2be88195fb85ced476467965 Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Wed, 15 Nov 2023 22:59:26 +0100 Subject: [PATCH 4/5] Perform mods verification during preparation for upcoming missions --- .../Services/IModsVerificationService.cs | 11 ++++++++++- .../Services/MissionPreparationService.cs | 7 +++++-- .../Services/ModsVerificationService.cs | 9 ++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs b/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs index 0899a5d..e3ff470 100644 --- a/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs +++ b/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using ArmaForces.Arma.Server.Features.Mods; using ArmaForces.Arma.Server.Features.Modsets; using CSharpFunctionalExtensions; @@ -26,4 +27,12 @@ public interface IModsVerificationService /// Cancellation token. /// Successful result if all mods were verified correctly. Task VerifyModset(string modsetName, CancellationToken cancellationToken); + + /// + /// Runs detailed verification of . + /// + /// List of mods to verify. + /// Cancellation token. + /// Successful result if all mods were verified correctly. + Task VerifyMods(IEnumerable mods, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ArmaForces.ArmaServerManager/Services/MissionPreparationService.cs b/ArmaForces.ArmaServerManager/Services/MissionPreparationService.cs index 5b72995..35148e7 100644 --- a/ArmaForces.ArmaServerManager/Services/MissionPreparationService.cs +++ b/ArmaForces.ArmaServerManager/Services/MissionPreparationService.cs @@ -20,19 +20,22 @@ public class MissionPreparationService : IMissionPreparationService private readonly IWebModsetMapper _webModsetMapper; private readonly IServerStartupService _serverStartupService; private readonly IModsUpdateService _modsUpdateService; + private readonly IModsVerificationService _modsVerificationService; public MissionPreparationService( IApiMissionsClient apiMissionsClient, IApiModsetClient apiModsetClient, IWebModsetMapper webModsetMapper, IServerStartupService serverStartupService, - IModsUpdateService modsUpdateService) + IModsUpdateService modsUpdateService, + IModsVerificationService modsVerificationService) { _apiMissionsClient = apiMissionsClient; _apiModsetClient = apiModsetClient; _webModsetMapper = webModsetMapper; _serverStartupService = serverStartupService; _modsUpdateService = modsUpdateService; + _modsVerificationService = modsVerificationService; } /// @@ -40,7 +43,7 @@ public async Task PrepareForUpcomingMissions(CancellationToken cancellat { return await _apiMissionsClient.GetUpcomingMissionsModsetsNames() .Bind(GetModsListFromModsets) - .Tap(x => _modsUpdateService.UpdateMods(x, cancellationToken)) + .Tap(x => _modsVerificationService.VerifyMods(x, cancellationToken)) .Bind(_ => Result.Success()); } diff --git a/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs b/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs index 58c5fff..d52b01d 100644 --- a/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs +++ b/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ArmaForces.Arma.Server.Features.Mods; using ArmaForces.Arma.Server.Features.Modsets; using ArmaForces.ArmaServerManager.Features.Mods; using ArmaForces.ArmaServerManager.Features.Modsets; @@ -33,5 +34,11 @@ public async Task VerifyModset(Modset modset, CancellationToken cancella { return await _modsManager.VerifyMods(modset.Mods.ToList(), cancellationToken); } + + /// + public async Task VerifyMods(IEnumerable mods, CancellationToken cancellationToken) + { + return await _modsManager.VerifyMods(mods.ToList(), cancellationToken); + } } } \ No newline at end of file From 73f95e7a8d9570557964d8f2e4b9eb6ba42fe268 Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Wed, 15 Nov 2023 23:47:37 +0100 Subject: [PATCH 5/5] Fix missing mods not returned as needing update --- .../Features/Mods/ModsManagerUnitTests.cs | 43 ++++++++++++++++++- .../Features/Mods/ModsManager.cs | 12 ++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs index 49279ea..c240ac4 100644 --- a/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs +++ b/ArmaForces.ArmaServerManager.Tests/Features/Mods/ModsManagerUnitTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -88,14 +89,25 @@ public async Task PrepareModset_SomeModsOutdated_UpdatedOutdatedMods() var outdatedMods = modset.Mods .Take(5) + .Select(x => { + x.LastUpdatedAt = DateTime.Now.AddDays(-10); + return x; + }) .ToList(); var upToDateMods = modset.Mods .Except(outdatedMods) + .Select(x => { + x.LastUpdatedAt = DateTime.Now.AddDays(10); + return x; + }) .ToList(); + + modset.Mods = outdatedMods.Concat(upToDateMods).ToHashSet(); - AddModsToModsCache(modset.Mods.ToList()); SetModsAsUpToDate(upToDateMods); + SetupPublishedFileDetails(modset.Mods); SetupContentDownloader(outdatedMods); + AddModsToModsCache(modset.Mods.ToList()); await _modsManager.PrepareModset(modset, cancellationToken); @@ -164,6 +176,24 @@ private void SetModsAsUpToDate(IReadOnlyCollection mods) .Returns(Task.FromResult(Result.Success(contentItem))); } } + + private void SetupPublishedFileDetails(ISet mods) + { + var modsIds = mods.Select(x => (ulong)x.WorkshopId!); + + var details = mods + .Select(x => new PublishedFileDetails + { + Title = x.Name, + PublishedFileId = x.WorkshopId!.Value, + LastUpdatedAt = DateTime.Now + }) + .ToArray(); + + _steamRemoteStorageMock + .Setup(x => x.GetPublishedFileDetails(modsIds, It.IsAny())) + .Returns(Task.FromResult(Result.Success(details))); + } private void AddModsToModsCache(IReadOnlyCollection mods) { @@ -173,6 +203,11 @@ private void AddModsToModsCache(IReadOnlyCollection mods) .Setup(x => x.ModExists(mod)) .Returns(Task.FromResult(true)); } + + var modsInCache = _modsCacheMock.Object.Mods; + _modsCacheMock + .Setup(x => x.Mods) + .Returns(modsInCache.Concat(mods).ToList()); } private Mock CreateModsCacheMock() @@ -182,6 +217,10 @@ private Mock CreateModsCacheMock() mock .Setup(x => x.ModExists(It.IsAny())) .Returns(Task.FromResult(false)); + + mock + .Setup(x => x.Mods) + .Returns(new List()); return mock; } @@ -214,7 +253,7 @@ private Mock CreateSteamRemoteStorageMock() mock .Setup(x => x.GetPublishedFileDetails(It.IsAny>(), It.IsAny())) - .Returns(Task.FromResult(new Result())); + .Returns(Task.FromResult(Result.Success(Array.Empty()))); return mock; } diff --git a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs index 4fcb884..0f99288 100644 --- a/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs +++ b/ArmaForces.ArmaServerManager/Features/Mods/ModsManager.cs @@ -77,9 +77,12 @@ public async Task>> CheckModsUpdated(IReadOnlyCollection m { return Result.Success(new List()); } - - var workshopModIds = modsList + + var workshopMods = modsList .Where(x => x.Source == ModSource.SteamWorkshop) + .ToList(); + + var workshopModIds = workshopMods .Select(x => x.WorkshopId) .Where(x => x.HasValue) .Select(x => (ulong)x!.Value) @@ -93,8 +96,11 @@ public async Task>> CheckModsUpdated(IReadOnlyCollection m .Where(x => x.mod.LastUpdatedAt < x.details.LastUpdatedAt) .Select(x => x.mod) .ToList(); + + var modsNotInCache = workshopMods + .Where(x => _modsCache.Mods.NotContains(x)); - return Result.Success(modsToUpdate); + return Result.Success(modsNotInCache.Concat(modsToUpdate).ToList()); } ///