From 4381c46bf170e4d0f789b33b0f8d7e7da9814fcb Mon Sep 17 00:00:00 2001 From: 3Mydlo3 Date: Sat, 14 Sep 2024 14:22:20 +0200 Subject: [PATCH] Quick-check mods for updates on server startup using workshop item updated date (#96) * Implement quick mods update to run before server startup * Fix incorrect media type error when making request without JSON body * Remove verifyMods from status * Perform mods verification during preparation for upcoming missions * Fix missing mods not returned as needing update --- .../Features/Mods/ModsManagerUnitTests.cs | 56 ++++++++++- .../Api/Jobs/DTOs/JobScheduleRequestDto.cs | 2 +- .../Api/Mods/ModsController.cs | 19 +++- .../Api/Status/DTOs/AppStatus.cs | 5 + .../ModsServiceCollectionExtensions.cs | 2 + .../Features/Mods/IModsManager.cs | 15 ++- .../Features/Mods/ModsManager.cs | 40 ++++++++ .../Features/Status/AppStatusStore.cs | 57 +++++++++++ .../Features/Status/StatusProvider.cs | 14 ++- .../RemoteStorage/ISteamRemoteStorage.cs | 98 +++++++++++++++++++ .../Properties/launchSettings.json | 2 + .../Services/IModsUpdateService.cs | 5 +- .../Services/IModsVerificationService.cs | 38 +++++++ .../Services/MissionPreparationService.cs | 7 +- .../Services/ModsVerificationService.cs | 24 ++++- .../Services/ServerStartupService.cs | 12 ++- ArmaForces.ArmaServerManager/Startup.cs | 2 + 17 files changed, 379 insertions(+), 19 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..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; @@ -8,6 +9,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 +26,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 +34,12 @@ public ModsManagerUnitTests() _modsCacheMock = CreateModsCacheMock(); _contentVerifierMock = CreateContentVerifierMock(); _downloaderMock = CreateContentDownloaderMock(); + _steamRemoteStorageMock = CreateSteamRemoteStorageMock(); _modsManager = new ModsManager( _downloaderMock.Object, _contentVerifierMock.Object, _modsCacheMock.Object, + _steamRemoteStorageMock.Object, new NullLogger()); } @@ -84,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); @@ -160,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) { @@ -169,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() @@ -178,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; } @@ -203,5 +246,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(Result.Success(Array.Empty()))); + + return mock; + } } } \ No newline at end of file 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; } } } 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..0f99288 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; } @@ -71,6 +78,39 @@ public async Task>> CheckModsUpdated(IReadOnlyCollection m return Result.Success(new List()); } + 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) + .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(); + + var modsNotInCache = workshopMods + .Where(x => _modsCache.Mods.NotContains(x)); + + return Result.Success(modsNotInCache.Concat(modsToUpdate).ToList()); + } + + /// + public async Task>> VerifyMods(IReadOnlyCollection modsList, CancellationToken cancellationToken) + { + if (modsList.IsEmpty()) + { + return Result.Success(new List()); + } + var modsRequireUpdate = new ConcurrentBag(); await foreach (var mod in modsList.Where(x => x.Source == ModSource.SteamWorkshop) 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..7b544c8 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,11 @@ 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(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..e3ff470 --- /dev/null +++ b/ArmaForces.ArmaServerManager/Services/IModsVerificationService.cs @@ -0,0 +1,38 @@ +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; + +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); + + /// + /// 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 285791b..d52b01d 100644 --- a/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs +++ b/ArmaForces.ArmaServerManager/Services/ModsVerificationService.cs @@ -1,5 +1,8 @@ -using System.Threading; +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; @@ -7,7 +10,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 +24,21 @@ 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); + } + + /// + public async Task VerifyMods(IEnumerable mods, CancellationToken cancellationToken) + { + return await _modsManager.VerifyMods(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