From 92a26600af316414c09dd3e5e08adef3245ecc5c Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 22 Apr 2020 18:46:41 -0500 Subject: [PATCH] actually fix the session score concurrency issue fix rare bug with shared guid kicker plugin allow hiding of the connection lost notification --- Application/IW4MServer.cs | 14 ++- Application/Misc/ScriptPlugin.cs | 4 +- Plugins/Stats/Models/EFClientStatistics.cs | 20 +--- .../Configuration/ApplicationConfiguration.cs | 2 + SharedLibraryCore/Server.cs | 2 +- SharedLibraryCore/Services/ClientService.cs | 3 +- .../DepedencyInjectionExtensions.cs | 37 ++++++++ .../Fixtures/ClientGenerators.cs | 22 ++++- Tests/ApplicationTests/IW4MServerTests.cs | 91 ++++++++++++++++--- Tests/ApplicationTests/PluginTests.cs | 86 ++++++++++++++++++ 10 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 Tests/ApplicationTests/DepedencyInjectionExtensions.cs create mode 100644 Tests/ApplicationTests/PluginTests.cs diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 3442d792f..34e29a889 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -236,10 +236,13 @@ namespace IW4MAdmin if (E.Type == GameEvent.EventType.ConnectionLost) { var exception = E.Extra as Exception; - Logger.WriteError(exception.Message); - if (exception.Data["internal_exception"] != null) + if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) { - Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}"); + Logger.WriteError(exception.Message); + if (exception.Data["internal_exception"] != null) + { + Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}"); + } } Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate"); Throttled = true; @@ -730,6 +733,7 @@ namespace IW4MAdmin override public async Task ProcessUpdatesAsync(CancellationToken cts) { + bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost; try { if (cts.IsCancellationRequested) @@ -796,7 +800,7 @@ namespace IW4MAdmin Manager.GetEventHandler().AddEvent(e); } - if (ConnectionErrors > 0) + if (ConnectionErrors > 0 && notifyDisconnects) { var _event = new GameEvent() { @@ -816,7 +820,7 @@ namespace IW4MAdmin catch (NetworkException e) { ConnectionErrors++; - if (ConnectionErrors == 3) + if (ConnectionErrors == 3 && notifyDisconnects) { var _event = new GameEvent() { diff --git a/Application/Misc/ScriptPlugin.cs b/Application/Misc/ScriptPlugin.cs index 9a6c7f3b5..472c2351f 100644 --- a/Application/Misc/ScriptPlugin.cs +++ b/Application/Misc/ScriptPlugin.cs @@ -35,12 +35,12 @@ namespace IW4MAdmin.Application.Misc private readonly SemaphoreSlim _onProcessing; private bool successfullyLoaded; - public ScriptPlugin(string filename) + public ScriptPlugin(string filename, string workingDirectory = null) { _fileName = filename; Watcher = new FileSystemWatcher() { - Path = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", + Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory, NotifyFilter = NotifyFilters.Size, Filter = _fileName.Split(Path.DirectorySeparatorChar).Last() }; diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index 635aa1710..83f1524b4 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -82,36 +82,24 @@ namespace IW4MAdmin.Plugins.Stats.Models KillStreak = 0; DeathStreak = 0; LastScore = 0; - lock (SessionScores) - { - SessionScores.Add(0); - } + SessionScores.Add(0); Team = IW4Info.Team.None; } [NotMapped] public int SessionScore { - set - { - SessionScores[SessionScores.Count - 1] = value; - } + set => SessionScores[SessionScores.Count - 1] = value; get { lock (SessionScores) { - return SessionScores.Sum(); + return new List(SessionScores).Sum(); } } } [NotMapped] - public int RoundScore - { - get - { - return SessionScores[SessionScores.Count - 1]; - } - } + public int RoundScore => SessionScores[SessionScores.Count - 1]; [NotMapped] private readonly List SessionScores = new List() { 0 }; [NotMapped] diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 83b049cc0..51d2dc253 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -96,6 +96,8 @@ namespace SharedLibraryCore.Configuration public QuickMessageConfiguration[] QuickMessages { get; set; } [ConfigurationIgnore] public string WebfrontUrl => string.IsNullOrEmpty(ManualWebfrontUrl) ? WebfrontBindUrl?.Replace("0.0.0.0", "127.0.0.1") : ManualWebfrontUrl; + [ConfigurationIgnore] + public bool IgnoreServerConnectionLost { get; set; } public IBaseConfiguration Generate() { diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index ee176b7ca..687151c3a 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -285,7 +285,7 @@ namespace SharedLibraryCore public List Reports { get; set; } public List ChatHistory { get; protected set; } public Queue ClientHistory { get; private set; } - public Game GameName { get; protected set; } + public Game GameName { get; set; } // Info public string Hostname { get; protected set; } diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index e58ab242b..c9f43b622 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -374,6 +374,7 @@ namespace SharedLibraryCore.Services EF.CompileAsyncQuery((DatabaseContext context, long networkId) => context.Clients .Include(c => c.CurrentAlias) + .Include(c => c.AliasLink) .Select(_client => new EFClient() { ClientId = _client.ClientId, @@ -389,7 +390,7 @@ namespace SharedLibraryCore.Services .FirstOrDefault(c => c.NetworkId == networkId) ); - public async Task GetUnique(long entityAttribute) + public virtual async Task GetUnique(long entityAttribute) { using (var context = new DatabaseContext(true)) { diff --git a/Tests/ApplicationTests/DepedencyInjectionExtensions.cs b/Tests/ApplicationTests/DepedencyInjectionExtensions.cs new file mode 100644 index 000000000..684cfe541 --- /dev/null +++ b/Tests/ApplicationTests/DepedencyInjectionExtensions.cs @@ -0,0 +1,37 @@ +using ApplicationTests.Fixtures; +using FakeItEasy; +using IW4MAdmin; +using Microsoft.Extensions.DependencyInjection; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Services; + +namespace ApplicationTests +{ + static class DepedencyInjectionExtensions + { + public static IServiceCollection BuildBase(this IServiceCollection serviceCollection) + { + var manager = A.Fake(); + var logger = A.Fake(); + A.CallTo(() => manager.GetLogger(A.Ignored)) + .Returns(logger); + + serviceCollection.AddSingleton(logger) + .AddSingleton(manager) + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()); + + serviceCollection.AddSingleton(_sp => new IW4MServer(_sp.GetRequiredService(), ConfigurationGenerators.CreateServerConfiguration(), + _sp.GetRequiredService(), _sp.GetRequiredService()) + { + RconParser = _sp.GetRequiredService() + }); + + return serviceCollection; + } + } +} diff --git a/Tests/ApplicationTests/Fixtures/ClientGenerators.cs b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs index 29c0c41fe..0c8b33f88 100644 --- a/Tests/ApplicationTests/Fixtures/ClientGenerators.cs +++ b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs @@ -8,17 +8,35 @@ namespace ApplicationTests.Fixtures { public class ClientGenerators { - public static EFClient CreateBasicClient(Server currentServer, bool isIngame = true) => new EFClient() + public static EFClient CreateBasicClient(Server currentServer, bool isIngame = true, bool hasIp = true, EFClient.ClientState clientState = EFClient.ClientState.Connected) => new EFClient() { ClientId = 1, CurrentAlias = new EFAlias() { Name = "BasicClient", - IPAddress = "127.0.0.1".ConvertToIP(), + IPAddress = hasIp ? "127.0.0.1".ConvertToIP() : null, }, Level = EFClient.Permission.User, ClientNumber = isIngame ? 0 : -1, CurrentServer = currentServer }; + + public static EFClient CreateDatabaseClient(bool hasIp = true) => new EFClient() + { + ClientId = 1, + ClientNumber = -1, + AliasLinkId = 1, + Level = EFClient.Permission.User, + Connections = 1, + FirstConnection = DateTime.UtcNow.AddDays(-1), + LastConnection = DateTime.UtcNow, + NetworkId = 1, + TotalConnectionTime = 100, + CurrentAlias = new EFAlias() + { + Name = "BasicDatabaseClient", + IPAddress = hasIp ? "127.0.0.1".ConvertToIP() : null, + }, + }; } } diff --git a/Tests/ApplicationTests/IW4MServerTests.cs b/Tests/ApplicationTests/IW4MServerTests.cs index 9688a1b4b..b0cc1d50f 100644 --- a/Tests/ApplicationTests/IW4MServerTests.cs +++ b/Tests/ApplicationTests/IW4MServerTests.cs @@ -11,6 +11,9 @@ using SharedLibraryCore.Database.Models; using System.Threading.Tasks; using ApplicationTests.Mocks; using System.Linq; +using SharedLibraryCore; +using SharedLibraryCore.Exceptions; +using SharedLibraryCore.Configuration; namespace ApplicationTests { @@ -27,29 +30,28 @@ namespace ApplicationTests [SetUp] public void Setup() { - fakeLogger = A.Fake(); - fakeManager = A.Fake(); - fakeRConConnection = A.Fake(); - var rconConnectionFactory = A.Fake(); + serviceProvider = new ServiceCollection().BuildBase().BuildServiceProvider(); + + fakeLogger = serviceProvider.GetRequiredService(); + fakeManager = serviceProvider.GetRequiredService(); + fakeRConConnection = serviceProvider.GetRequiredService(); + fakeRConParser = serviceProvider.GetRequiredService(); + + var rconConnectionFactory = serviceProvider.GetRequiredService(); + A.CallTo(() => rconConnectionFactory.CreateConnection(A.Ignored, A.Ignored, A.Ignored)) .Returns(fakeRConConnection); - var fakeTranslationLookup = A.Fake(); - fakeRConParser = A.Fake(); + A.CallTo(() => fakeRConParser.Configuration) - .Returns(ConfigurationGenerators.CreateRConParserConfiguration(A.Fake())); + .Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService())); + mockEventHandler = new MockEventHandler(); A.CallTo(() => fakeManager.GetEventHandler()) .Returns(mockEventHandler); - - serviceProvider = new ServiceCollection() - .AddSingleton(new IW4MServer(fakeManager, ConfigurationGenerators.CreateServerConfiguration(), fakeTranslationLookup, rconConnectionFactory) - { - RconParser = fakeRConParser - }) - .BuildServiceProvider(); } + #region LOG [Test] public void Test_GenerateLogPath_Basic() { @@ -176,6 +178,7 @@ namespace ApplicationTests Assert.AreEqual(expected, generated); } + #endregion #region BAN [Test] @@ -508,5 +511,65 @@ namespace ApplicationTests .MustHaveHappened(); } #endregion + + [Test] + public async Task Test_ConnectionLostNotificationDisabled() + { + var server = serviceProvider.GetService(); + var fakeConfigHandler = A.Fake>(); + + A.CallTo(() => fakeManager.GetApplicationSettings()) + .Returns(fakeConfigHandler); + + A.CallTo(() => fakeConfigHandler.Configuration()) + .Returns(new ApplicationConfiguration() { IgnoreServerConnectionLost = true }); + + A.CallTo(() => fakeRConParser.GetStatusAsync(A.Ignored)) + .ThrowsAsync(new NetworkException("err")); + + // simulate failed connection attempts + for (int i = 0; i < 5; i++) + { + await server.ProcessUpdatesAsync(new System.Threading.CancellationToken()); + } + + A.CallTo(() => fakeLogger.WriteError(A.Ignored)) + .MustNotHaveHappened(); + Assert.IsEmpty(mockEventHandler.Events); + } + + [Test] + public async Task Test_ConnectionLostNotificationEnabled() + { + var server = serviceProvider.GetService(); + var fakeConfigHandler = A.Fake>(); + + A.CallTo(() => fakeManager.GetApplicationSettings()) + .Returns(fakeConfigHandler); + + A.CallTo(() => fakeConfigHandler.Configuration()) + .Returns(new ApplicationConfiguration() { IgnoreServerConnectionLost = false }); + + A.CallTo(() => fakeRConParser.GetStatusAsync(A.Ignored)) + .ThrowsAsync(new NetworkException("err")); + + // simulate failed connection attempts + for (int i = 0; i < 5; i++) + { + await server.ProcessUpdatesAsync(new System.Threading.CancellationToken()); + } + + // execute the connection lost event + foreach(var e in mockEventHandler.Events.ToList()) + { + await server.ExecuteEvent(e); + } + + A.CallTo(() => fakeLogger.WriteError(A.Ignored)) + .MustHaveHappenedOnceExactly(); + + Assert.IsNotEmpty(mockEventHandler.Events); + Assert.AreEqual("err", (mockEventHandler.Events[0].Extra as NetworkException).Message); + } } } diff --git a/Tests/ApplicationTests/PluginTests.cs b/Tests/ApplicationTests/PluginTests.cs new file mode 100644 index 000000000..f9ea27b6b --- /dev/null +++ b/Tests/ApplicationTests/PluginTests.cs @@ -0,0 +1,86 @@ +using ApplicationTests.Fixtures; +using ApplicationTests.Mocks; +using FakeItEasy; +using IW4MAdmin; +using IW4MAdmin.Application.Misc; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ApplicationTests +{ + [TestFixture] + public class PluginTests + { + private static string PLUGIN_DIR = @"X:\IW4MAdmin\Plugins\ScriptPlugins"; + private IServiceProvider serviceProvider; + private IManager fakeManager; + private MockEventHandler mockEventHandler; + + [SetUp] + public void Setup() + { + serviceProvider = new ServiceCollection().BuildBase().BuildServiceProvider(); + fakeManager = serviceProvider.GetRequiredService(); + mockEventHandler = new MockEventHandler(); + A.CallTo(() => fakeManager.GetEventHandler()) + .Returns(mockEventHandler); + + var rconConnectionFactory = serviceProvider.GetRequiredService(); + + A.CallTo(() => rconConnectionFactory.CreateConnection(A.Ignored, A.Ignored, A.Ignored)) + .Returns(serviceProvider.GetRequiredService()); + + A.CallTo(() => serviceProvider.GetRequiredService().Configuration) + .Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService())); + } + + [Test] + public async Task Test_GenericGuidClientIsKicked() + { + var plugin = new ScriptPlugin(Path.Join(PLUGIN_DIR, "SharedGUIDKick.js"), PLUGIN_DIR); + var server = serviceProvider.GetRequiredService(); + server.GameName = Server.Game.IW4; + var client = ClientGenerators.CreateBasicClient(server, hasIp: false, clientState: EFClient.ClientState.Connecting); + client.NetworkId = -1168897558496584395; + var databaseClient = ClientGenerators.CreateDatabaseClient(hasIp: false); + databaseClient.NetworkId = client.NetworkId; + + var fakeClientService = serviceProvider.GetRequiredService(); + A.CallTo(() => fakeClientService.GetUnique(A.Ignored)) + .Returns(Task.FromResult(databaseClient)); + A.CallTo(() => fakeManager.GetClientService()) + .Returns(fakeClientService); + + await plugin.Initialize(serviceProvider.GetRequiredService()); + + var gameEvent = new GameEvent() + { + Origin = client, + Owner = server, + Type = GameEvent.EventType.PreConnect, + IsBlocking = true + }; + + await server.ExecuteEvent(gameEvent); + + // connect + var e = mockEventHandler.Events[0]; + await server.ExecuteEvent(e); + await plugin.OnEventAsync(e, server); + + // kick + e = mockEventHandler.Events[1]; + await server.ExecuteEvent(e); + } + } +}