diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs
index 9ccd8dfc..db4cce29 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 00000000..631b7b5a
--- /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 8db6389e..5044694c 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 0082763b..b02f7305 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 9fd94e87..1c176863 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 837d4aa6..faac62f1 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 359467ae..ad3843d0 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 2febd0ed..a4c501f1 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 c56f8276..b6cce985 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 bfe18a68..f712a6dc 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 857ddcbb..f6251584 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 b1c86a05..6aeb76ea 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 8a8923c8..dfee57b1 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 a0cc3d7c..a2cd41a0 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 4da0714c..58e9165b 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 2e158526..b3ed94e9 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 cee40ed8..cd7c2b94 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 00000000..179f4afb
--- /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 00000000..7ca1df8c
--- /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 7dca84d5..c8c0e58f 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 80a2fabc..4885cabf 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 684cfe54..daafc2bb 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 0c8b33f8..4cb132a8 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 00000000..ee176b36
--- /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 9e783ead..d9759e2f 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 558f2c74..1536d1d5 100644
--- a/WebfrontCore/Views/Server/_ClientActivity.cshtml
+++ b/WebfrontCore/Views/Server/_ClientActivity.cshtml
@@ -36,7 +36,7 @@
—
-
+
}
}
@@ -113,7 +113,7 @@
—
-
+
}
}