From 5529858edd8d8b4c71479624d96cfd37f6a04f70 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 25 Apr 2020 19:01:26 -0500 Subject: [PATCH] [misc bug fixes] properly hide broadcast failure messages if ignore connection lost is turned on fix concurent issue for update stats history that happened with new event processing make get/set additional property thread safe add ellipse to truncated chat messages on home --- Application/ApplicationManager.cs | 6 ++ .../Factories/DatabaseContextFactory.cs | 21 ++++++ Application/IW4MServer.cs | 19 ++++-- Application/Main.cs | 1 + .../AutomessageFeed/AutomessageFeed.csproj | 2 +- .../IW4ScriptCommands.csproj | 2 +- Plugins/LiveRadar/LiveRadar.csproj | 2 +- Plugins/Login/Login.csproj | 2 +- .../ProfanityDeterment.csproj | 2 +- Plugins/Stats/Commands/ViewStats.cs | 4 +- Plugins/Stats/Helpers/StatManager.cs | 65 +++++++++++++------ Plugins/Stats/Plugin.cs | 8 ++- Plugins/Stats/Stats.csproj | 2 +- Plugins/Web/StatsWeb/StatsWeb.csproj | 2 +- Plugins/Welcome/Welcome.csproj | 2 +- SharedLibraryCore/Database/DatabaseContext.cs | 5 +- .../Database/Models/SharedEntity.cs | 30 ++++++++- .../Interfaces/IDatabaseContextFactory.cs | 17 +++++ .../Interfaces/IPropertyExtender.cs | 23 +++++++ SharedLibraryCore/PartialEntities/EFClient.cs | 27 +------- SharedLibraryCore/SharedLibraryCore.csproj | 6 +- .../DepedencyInjectionExtensions.cs | 4 ++ .../Fixtures/ClientGenerators.cs | 2 +- .../Mocks/DatabaseContextFactoryMock.cs | 32 +++++++++ Tests/ApplicationTests/StatsTests.cs | 42 +++++++++++- .../Views/Server/_ClientActivity.cshtml | 4 +- 26 files changed, 258 insertions(+), 74 deletions(-) create mode 100644 Application/Factories/DatabaseContextFactory.cs create mode 100644 SharedLibraryCore/Interfaces/IDatabaseContextFactory.cs create mode 100644 SharedLibraryCore/Interfaces/IPropertyExtender.cs create mode 100644 Tests/ApplicationTests/Mocks/DatabaseContextFactoryMock.cs diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 9ccd8dfc9..db4cce295 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -480,6 +480,12 @@ namespace IW4MAdmin.Application var client = await GetClientService().Get(clientId); + if (client == null) + { + _logger.WriteWarning($"No client found with id {clientId} when generating profile meta"); + return metaList; + } + metaList.Add(new ProfileMeta() { Id = client.ClientId, diff --git a/Application/Factories/DatabaseContextFactory.cs b/Application/Factories/DatabaseContextFactory.cs new file mode 100644 index 000000000..631b7b5a1 --- /dev/null +++ b/Application/Factories/DatabaseContextFactory.cs @@ -0,0 +1,21 @@ +using SharedLibraryCore.Database; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.Factories +{ + /// + /// implementation of the IDatabaseContextFactory interface + /// + public class DatabaseContextFactory : IDatabaseContextFactory + { + /// + /// creates a new database context + /// + /// indicates if entity tracking should be enabled + /// + public DatabaseContext CreateContext(bool? enableTracking = true) + { + return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext(); + } + } +} diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 8db6389e0..5044694ce 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -216,8 +216,11 @@ namespace IW4MAdmin if (lastException != null) { - Logger.WriteDebug("Last Exception is not null"); - throw lastException; + bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost; + if (notifyDisconnects || (!notifyDisconnects && lastException as NetworkException == null)) + { + throw lastException; + } } } } @@ -250,7 +253,7 @@ namespace IW4MAdmin if (E.Type == GameEvent.EventType.ConnectionRestored) { - if (Throttled) + if (Throttled && !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) { Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); } @@ -292,6 +295,12 @@ namespace IW4MAdmin return false; } + if (E.Origin.CurrentServer == null) + { + Logger.WriteWarning($"preconnecting client {E.Origin} did not have a current server specified"); + E.Origin.CurrentServer = this; + } + var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); // they're already connected @@ -800,7 +809,7 @@ namespace IW4MAdmin Manager.GetEventHandler().AddEvent(e); } - if (ConnectionErrors > 0 && notifyDisconnects) + if (ConnectionErrors > 0) { var _event = new GameEvent() { @@ -820,7 +829,7 @@ namespace IW4MAdmin catch (NetworkException e) { ConnectionErrors++; - if (ConnectionErrors == 3 && notifyDisconnects) + if (ConnectionErrors == 3) { var _event = new GameEvent() { diff --git a/Application/Main.cs b/Application/Main.cs index 0082763b0..b02f73058 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -283,6 +283,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddTransient() .AddSingleton(_serviceProvider => { diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index 9fd94e874..1c1768635 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj index 837d4aa6a..faac62f1d 100644 --- a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj +++ b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index 359467ae9..ad3843d04 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index 2febd0edb..a4c501f1c 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -23,7 +23,7 @@ - + diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index c56f82769..b6cce9859 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index bfe18a682..f712a6dc2 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -52,7 +52,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands if (E.Target != null) { - int performanceRanking = await StatManager.GetClientOverallRanking(E.Target.ClientId); + int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId); string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Target))) @@ -72,7 +72,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands else { - int performanceRanking = await StatManager.GetClientOverallRanking(E.Origin.ClientId); + int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId); string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Origin))) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 857ddcbb9..f62515844 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -1,4 +1,5 @@ using IW4MAdmin.Plugins.Stats.Cheat; +using IW4MAdmin.Plugins.Stats.Config; using IW4MAdmin.Plugins.Stats.Models; using IW4MAdmin.Plugins.Stats.Web.Dtos; using Microsoft.EntityFrameworkCore; @@ -23,32 +24,36 @@ namespace IW4MAdmin.Plugins.Stats.Helpers private const int MAX_CACHED_HITS = 100; private readonly ConcurrentDictionary _servers; private readonly ILogger _log; + private readonly IDatabaseContextFactory _contextFactory; + private readonly IConfigurationHandler _configHandler; private static List serverModels; public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; - public StatManager(IManager mgr) + public StatManager(IManager mgr, IDatabaseContextFactory contextFactory, IConfigurationHandler configHandler) { _servers = new ConcurrentDictionary(); _log = mgr.GetLogger(0); + _contextFactory = contextFactory; + _configHandler = configHandler; } private void SetupServerIds() { - using (var ctx = new DatabaseContext(disableTracking: true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { serverModels = ctx.Set().ToList(); } } - public static Expression> GetRankingFunc(long? serverId = null) + public Expression> GetRankingFunc(long? serverId = null) { var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); return (r) => r.ServerId == serverId && r.When > fifteenDaysAgo && r.RatingHistory.Client.Level != EFClient.Permission.Banned && r.Newest && - r.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime; + r.ActivityAmount >= _configHandler.Configuration().TopPlayersMinPlayTime; } /// @@ -56,9 +61,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// /// client id of the player /// - public static async Task GetClientOverallRanking(int clientId) + public async Task GetClientOverallRanking(int clientId) { - using (var context = new DatabaseContext(true)) + using (var context = _contextFactory.CreateContext(enableTracking: false)) { var clientPerformance = await context.Set() .Where(r => r.RatingHistory.ClientId == clientId) @@ -83,7 +88,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public async Task> GetTopStats(int start, int count, long? serverId = null) { - using (var context = new DatabaseContext(true)) + using (var context = _contextFactory.CreateContext(enableTracking: false)) { // setup the query for the clients within the given rating range var iqClientRatings = (from rating in context.Set() @@ -192,7 +197,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers long serverId = GetIdForServer(sv); EFServer server; - using (var ctx = new DatabaseContext(disableTracking: true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { var serverSet = ctx.Set(); // get the server from the database if it exists, otherwise create and insert a new one @@ -275,7 +280,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers EFClientStatistics clientStats; - using (var ctx = new DatabaseContext(disableTracking: true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { var clientStatsSet = ctx.Set(); clientStats = clientStatsSet @@ -394,9 +399,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } - private static async Task SaveClientStats(EFClientStatistics clientStats) + private async Task SaveClientStats(EFClientStatistics clientStats) { - using (var ctx = new DatabaseContext()) + using (var ctx = _contextFactory.CreateContext()) { ctx.Update(clientStats); await ctx.SaveChangesAsync(); @@ -591,7 +596,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public async Task SaveHitCache(long serverId) { - using (var ctx = new DatabaseContext(true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { var server = _servers[serverId]; ctx.AddRange(server.HitCache.ToList()); @@ -659,7 +664,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { EFACSnapshot change; - using (var ctx = new DatabaseContext(true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot)) { @@ -731,8 +736,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // update their performance if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5) { - attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; - await UpdateStatHistory(attacker, attackerStats); + try + { + // kill event is not designated as blocking, so we should be able to enter and exit + // we need to make this thread safe because we can potentially have kills qualify + // for stat history update, but one is already processing that invalidates the original + await attacker.Lock(); + await UpdateStatHistory(attacker, attackerStats); + attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; + } + + catch (Exception e) + { + _log.WriteWarning($"Could not update stat history for {attacker}"); + _log.WriteDebug(e.GetExceptionInfo()); + } + + finally + { + attacker.Unlock(); + } } } @@ -742,7 +765,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// client to update /// stats of client that is being updated /// - private async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats) + public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats) { int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds; @@ -754,7 +777,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; - using (var ctx = new DatabaseContext()) + using (var ctx = _contextFactory.CreateContext(enableTracking: true)) { // select the rating history for client var iqHistoryLink = from history in ctx.Set() @@ -842,7 +865,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var clientStatsList = await iqClientStats.ToListAsync(); - // add the current server's so we don't have to pull it frmo the database + // add the current server's so we don't have to pull it from the database clientStatsList.Add(new { clientStats.Performance, @@ -1067,7 +1090,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { EFServerStatistics serverStats; - using (var ctx = new DatabaseContext(disableTracking: true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { var serverStatsSet = ctx.Set(); serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId); @@ -1119,7 +1142,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return; } - using (var ctx = new DatabaseContext(disableTracking: true)) + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) { ctx.Set().Add(new EFClientMessage() { @@ -1142,7 +1165,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { await waiter.WaitAsync(); - using (var ctx = new DatabaseContext()) + using (var ctx = _contextFactory.CreateContext()) { var serverStatsSet = ctx.Set(); serverStatsSet.Update(_servers[serverId].ServerStatistics); diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index b1c86a055..6aeb76eae 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -31,10 +31,12 @@ namespace IW4MAdmin.Plugins.Stats int scriptDamageCount; int scriptKillCount; #endif + private readonly IDatabaseContextFactory _databaseContextFactory; - public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory) { Config = configurationHandlerFactory.GetConfigurationHandler("StatsPluginSettings"); + _databaseContextFactory = databaseContextFactory; } public async Task OnEventAsync(GameEvent E, Server S) @@ -209,7 +211,7 @@ namespace IW4MAdmin.Plugins.Stats new ProfileMeta() { Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"], - Value = "#" + (await StatManager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), + Value = "#" + (await Manager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Column = 0, Order = 0, Type = ProfileMeta.MetaType.Information @@ -495,7 +497,7 @@ namespace IW4MAdmin.Plugins.Stats manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", mostPlayed)); ServerManager = manager; - Manager = new StatManager(manager); + Manager = new StatManager(manager, _databaseContextFactory, Config); } public Task OnTickAsync(Server S) diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 8a8923c8e..dfee57b16 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Web/StatsWeb/StatsWeb.csproj b/Plugins/Web/StatsWeb/StatsWeb.csproj index a0cc3d7cb..a2cd41a03 100644 --- a/Plugins/Web/StatsWeb/StatsWeb.csproj +++ b/Plugins/Web/StatsWeb/StatsWeb.csproj @@ -14,7 +14,7 @@ Always - + diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index 4da0714c9..58e9165b9 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -16,7 +16,7 @@ - + diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs index 2e158526d..b3ed94e9d 100644 --- a/SharedLibraryCore/Database/DatabaseContext.cs +++ b/SharedLibraryCore/Database/DatabaseContext.cs @@ -86,7 +86,10 @@ namespace SharedLibraryCore.Database var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); - optionsBuilder.UseSqlite(connection); + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlite(connection); + } } else diff --git a/SharedLibraryCore/Database/Models/SharedEntity.cs b/SharedLibraryCore/Database/Models/SharedEntity.cs index cee40ed84..cd7c2b947 100644 --- a/SharedLibraryCore/Database/Models/SharedEntity.cs +++ b/SharedLibraryCore/Database/Models/SharedEntity.cs @@ -1,15 +1,41 @@ -using System; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations.Schema; namespace SharedLibraryCore.Database.Models { - public class SharedEntity + public class SharedEntity : IPropertyExtender { + private readonly ConcurrentDictionary _additionalProperties; + /// /// indicates if the entity is active /// public bool Active { get; set; } = true; + public SharedEntity() + { + _additionalProperties = new ConcurrentDictionary(); + } + + public T GetAdditionalProperty(string name) + { + return _additionalProperties.ContainsKey(name) ? (T)_additionalProperties[name] : default; + } + + public void SetAdditionalProperty(string name, object value) + { + if (_additionalProperties.ContainsKey(name)) + { + _additionalProperties[name] = value; + } + else + { + _additionalProperties.TryAdd(name, value); + } + } + ///// ///// Specifies when the entity was created ///// diff --git a/SharedLibraryCore/Interfaces/IDatabaseContextFactory.cs b/SharedLibraryCore/Interfaces/IDatabaseContextFactory.cs new file mode 100644 index 000000000..179f4afb6 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IDatabaseContextFactory.cs @@ -0,0 +1,17 @@ +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// describes the capabilities of the database context factory + /// + public interface IDatabaseContextFactory + { + /// + /// create or retrieves an existing database context instance + /// + /// indicated if entity tracking should be enabled + /// database context instance + DatabaseContext CreateContext(bool? enableTracking = true); + } +} diff --git a/SharedLibraryCore/Interfaces/IPropertyExtender.cs b/SharedLibraryCore/Interfaces/IPropertyExtender.cs new file mode 100644 index 000000000..7ca1df8ce --- /dev/null +++ b/SharedLibraryCore/Interfaces/IPropertyExtender.cs @@ -0,0 +1,23 @@ +namespace SharedLibraryCore.Interfaces +{ + /// + /// describes the capability of extending properties by name + /// + interface IPropertyExtender + { + /// + /// adds or updates property by name + /// + /// unique name of the property + /// value of the property + void SetAdditionalProperty(string name, object value); + + /// + /// retreives a property by name + /// + /// + /// name of the property + /// property value if exists, otherwise default T + T GetAdditionalProperty(string name); + } +} diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index 7dca84d57..c8c0e58fc 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -86,10 +86,7 @@ namespace SharedLibraryCore.Database.Models { ConnectionTime = DateTime.UtcNow; ClientNumber = -1; - _additionalProperties = new Dictionary - { - { "_reportCount", 0 } - }; + SetAdditionalProperty("_reportCount", 0); ReceivedPenalties = new List(); _processingEvent = new SemaphoreSlim(1, 1); } @@ -101,7 +98,7 @@ namespace SharedLibraryCore.Database.Models public override string ToString() { - return $"{CurrentAlias?.Name ?? "--"}::{NetworkId}"; + return $"[Name={CurrentAlias?.Name ?? "--"}, NetworkId={NetworkId.ToString("X")}, IP={(string.IsNullOrEmpty(IPAddressString) ? "--" : IPAddressString)}, ClientSlot={ClientNumber}]"; } [NotMapped] @@ -643,26 +640,6 @@ namespace SharedLibraryCore.Database.Models return true; } - [NotMapped] - readonly Dictionary _additionalProperties; - - public T GetAdditionalProperty(string name) - { - return _additionalProperties.ContainsKey(name) ? (T)_additionalProperties[name] : default(T); - } - - public void SetAdditionalProperty(string name, object value) - { - if (_additionalProperties.ContainsKey(name)) - { - _additionalProperties[name] = value; - } - else - { - _additionalProperties.Add(name, value); - } - } - [NotMapped] public int ClientNumber { get; set; } [NotMapped] diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 80a2fabca..4885cabfb 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -6,7 +6,7 @@ RaidMax.IW4MAdmin.SharedLibraryCore - 2.2.8 + 2.2.10 RaidMax Forever None Debug;Release;Prerelease @@ -20,8 +20,8 @@ true MIT Shared Library for IW4MAdmin - 2.2.8.0 - 2.2.8.0 + 2.2.10.0 + 2.2.10.0 diff --git a/Tests/ApplicationTests/DepedencyInjectionExtensions.cs b/Tests/ApplicationTests/DepedencyInjectionExtensions.cs index 684cfe541..daafc2bb9 100644 --- a/Tests/ApplicationTests/DepedencyInjectionExtensions.cs +++ b/Tests/ApplicationTests/DepedencyInjectionExtensions.cs @@ -1,7 +1,9 @@ using ApplicationTests.Fixtures; +using ApplicationTests.Mocks; using FakeItEasy; using IW4MAdmin; using Microsoft.Extensions.DependencyInjection; +using SharedLibraryCore.Database; using SharedLibraryCore.Interfaces; using SharedLibraryCore.Services; @@ -13,11 +15,13 @@ namespace ApplicationTests { var manager = A.Fake(); var logger = A.Fake(); + A.CallTo(() => manager.GetLogger(A.Ignored)) .Returns(logger); serviceCollection.AddSingleton(logger) .AddSingleton(manager) + .AddSingleton() .AddSingleton(A.Fake()) .AddSingleton(A.Fake()) .AddSingleton(A.Fake()) diff --git a/Tests/ApplicationTests/Fixtures/ClientGenerators.cs b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs index 0c8b33f88..4cb132a8f 100644 --- a/Tests/ApplicationTests/Fixtures/ClientGenerators.cs +++ b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs @@ -29,7 +29,7 @@ namespace ApplicationTests.Fixtures Level = EFClient.Permission.User, Connections = 1, FirstConnection = DateTime.UtcNow.AddDays(-1), - LastConnection = DateTime.UtcNow, + LastConnection = DateTime.UtcNow.AddMinutes(-5), NetworkId = 1, TotalConnectionTime = 100, CurrentAlias = new EFAlias() diff --git a/Tests/ApplicationTests/Mocks/DatabaseContextFactoryMock.cs b/Tests/ApplicationTests/Mocks/DatabaseContextFactoryMock.cs new file mode 100644 index 000000000..ee176b363 --- /dev/null +++ b/Tests/ApplicationTests/Mocks/DatabaseContextFactoryMock.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Database; +using SharedLibraryCore.Interfaces; +using System; + +namespace ApplicationTests.Mocks +{ + class DatabaseContextFactoryMock : IDatabaseContextFactory + { + private DatabaseContext ctx; + private readonly IServiceProvider _serviceProvider; + + public DatabaseContextFactoryMock(IServiceProvider sp) + { + _serviceProvider = sp; + } + + public DatabaseContext CreateContext(bool? enableTracking) + { + if (ctx == null) + { + var contextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "database") + .Options; + + ctx = new DatabaseContext(contextOptions); + } + + return ctx; + } + } +} diff --git a/Tests/ApplicationTests/StatsTests.cs b/Tests/ApplicationTests/StatsTests.cs index 9e783ead8..d9759e2fe 100644 --- a/Tests/ApplicationTests/StatsTests.cs +++ b/Tests/ApplicationTests/StatsTests.cs @@ -10,6 +10,10 @@ using IW4MAdmin.Application.Helpers; using IW4MAdmin.Plugins.Stats.Config; using System.Collections.Generic; using SharedLibraryCore.Database.Models; +using Microsoft.Extensions.DependencyInjection; +using IW4MAdmin.Plugins.Stats.Helpers; +using ApplicationTests.Fixtures; +using System.Threading.Tasks; namespace ApplicationTests { @@ -17,12 +21,17 @@ namespace ApplicationTests public class StatsTests { ILogger logger; + private IServiceProvider serviceProvider; [SetUp] public void Setup() { logger = A.Fake(); + serviceProvider = new ServiceCollection() + .BuildBase() + .BuildServiceProvider(); + void testLog(string msg) => Console.WriteLine(msg); A.CallTo(() => logger.WriteError(A.Ignored)).Invokes((string msg) => testLog(msg)); @@ -37,7 +46,7 @@ namespace ApplicationTests var mgr = A.Fake(); var handlerFactory = A.Fake(); var config = A.Fake>(); - var plugin = new IW4MAdmin.Plugins.Stats.Plugin(handlerFactory); + var plugin = new IW4MAdmin.Plugins.Stats.Plugin(handlerFactory, null); A.CallTo(() => config.Configuration()) .Returns(new StatsConfiguration() @@ -113,5 +122,36 @@ namespace ApplicationTests public string BasePath => @"X:\IW4MAdmin\BUILD\Plugins"; } + [Test] + public async Task Test_ConcurrentCallsToUpdateStatHistoryDoesNotCauseException() + { + var server = serviceProvider.GetRequiredService(); + var configHandler = A.Fake>(); + var mgr = new StatManager(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), configHandler); + var target = ClientGenerators.CreateDatabaseClient(); + target.CurrentServer = server; + + A.CallTo(() => configHandler.Configuration()) + .Returns(new StatsConfiguration() + { + TopPlayersMinPlayTime = 0 + }); + + var dbFactory = serviceProvider.GetRequiredService(); + var db = dbFactory.CreateContext(true); + db.Set().Add(new EFServer() + { + EndPoint = server.EndPoint.ToString() + }); + + db.Clients.Add(target); + db.SaveChanges(); + + mgr.AddServer(server); + await mgr.AddPlayer(target); + var stats = target.GetAdditionalProperty("ClientStats"); + + await mgr.UpdateStatHistory(target, stats); + } } } diff --git a/WebfrontCore/Views/Server/_ClientActivity.cshtml b/WebfrontCore/Views/Server/_ClientActivity.cshtml index 558f2c742..1536d1d5f 100644 --- a/WebfrontCore/Views/Server/_ClientActivity.cshtml +++ b/WebfrontCore/Views/Server/_ClientActivity.cshtml @@ -36,7 +36,7 @@ — - +
} } @@ -113,7 +113,7 @@ — - +
} }