actually fix the session score concurrency issue

fix rare bug with shared guid kicker plugin
allow hiding of the connection lost notification
This commit is contained in:
RaidMax 2020-04-22 18:46:41 -05:00
parent 9e74dac5ed
commit 92a26600af
10 changed files with 240 additions and 41 deletions

View File

@ -236,11 +236,14 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.ConnectionLost)
{
var exception = E.Extra as Exception;
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
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<bool> 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()
{

View File

@ -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()
};

View File

@ -82,36 +82,24 @@ namespace IW4MAdmin.Plugins.Stats.Models
KillStreak = 0;
DeathStreak = 0;
LastScore = 0;
lock (SessionScores)
{
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<int>(SessionScores).Sum();
}
}
}
[NotMapped]
public int RoundScore
{
get
{
return SessionScores[SessionScores.Count - 1];
}
}
public int RoundScore => SessionScores[SessionScores.Count - 1];
[NotMapped]
private readonly List<int> SessionScores = new List<int>() { 0 };
[NotMapped]

View File

@ -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()
{

View File

@ -285,7 +285,7 @@ namespace SharedLibraryCore
public List<Report> Reports { get; set; }
public List<ChatInfo> ChatHistory { get; protected set; }
public Queue<PlayerHistory> ClientHistory { get; private set; }
public Game GameName { get; protected set; }
public Game GameName { get; set; }
// Info
public string Hostname { get; protected set; }

View File

@ -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<EFClient> GetUnique(long entityAttribute)
public virtual async Task<EFClient> GetUnique(long entityAttribute)
{
using (var context = new DatabaseContext(true))
{

View File

@ -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<IManager>();
var logger = A.Fake<ILogger>();
A.CallTo(() => manager.GetLogger(A<long>.Ignored))
.Returns(logger);
serviceCollection.AddSingleton(logger)
.AddSingleton(manager)
.AddSingleton(A.Fake<IRConConnectionFactory>())
.AddSingleton(A.Fake<IRConConnection>())
.AddSingleton(A.Fake<ITranslationLookup>())
.AddSingleton(A.Fake<IRConParser>())
.AddSingleton(A.Fake<IParserRegexFactory>())
.AddSingleton(A.Fake<ClientService>());
serviceCollection.AddSingleton(_sp => new IW4MServer(_sp.GetRequiredService<IManager>(), ConfigurationGenerators.CreateServerConfiguration(),
_sp.GetRequiredService<ITranslationLookup>(), _sp.GetRequiredService<IRConConnectionFactory>())
{
RconParser = _sp.GetRequiredService<IRConParser>()
});
return serviceCollection;
}
}
}

View File

@ -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,
},
};
}
}

View File

@ -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<ILogger>();
fakeManager = A.Fake<IManager>();
fakeRConConnection = A.Fake<IRConConnection>();
var rconConnectionFactory = A.Fake<IRConConnectionFactory>();
serviceProvider = new ServiceCollection().BuildBase().BuildServiceProvider();
fakeLogger = serviceProvider.GetRequiredService<ILogger>();
fakeManager = serviceProvider.GetRequiredService<IManager>();
fakeRConConnection = serviceProvider.GetRequiredService<IRConConnection>();
fakeRConParser = serviceProvider.GetRequiredService<IRConParser>();
var rconConnectionFactory = serviceProvider.GetRequiredService<IRConConnectionFactory>();
A.CallTo(() => rconConnectionFactory.CreateConnection(A<string>.Ignored, A<int>.Ignored, A<string>.Ignored))
.Returns(fakeRConConnection);
var fakeTranslationLookup = A.Fake<ITranslationLookup>();
fakeRConParser = A.Fake<IRConParser>();
A.CallTo(() => fakeRConParser.Configuration)
.Returns(ConfigurationGenerators.CreateRConParserConfiguration(A.Fake<IParserRegexFactory>()));
.Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService<IParserRegexFactory>()));
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<IW4MServer>();
var fakeConfigHandler = A.Fake<IConfigurationHandler<ApplicationConfiguration>>();
A.CallTo(() => fakeManager.GetApplicationSettings())
.Returns(fakeConfigHandler);
A.CallTo(() => fakeConfigHandler.Configuration())
.Returns(new ApplicationConfiguration() { IgnoreServerConnectionLost = true });
A.CallTo(() => fakeRConParser.GetStatusAsync(A<IRConConnection>.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<string>.Ignored))
.MustNotHaveHappened();
Assert.IsEmpty(mockEventHandler.Events);
}
[Test]
public async Task Test_ConnectionLostNotificationEnabled()
{
var server = serviceProvider.GetService<IW4MServer>();
var fakeConfigHandler = A.Fake<IConfigurationHandler<ApplicationConfiguration>>();
A.CallTo(() => fakeManager.GetApplicationSettings())
.Returns(fakeConfigHandler);
A.CallTo(() => fakeConfigHandler.Configuration())
.Returns(new ApplicationConfiguration() { IgnoreServerConnectionLost = false });
A.CallTo(() => fakeRConParser.GetStatusAsync(A<IRConConnection>.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<string>.Ignored))
.MustHaveHappenedOnceExactly();
Assert.IsNotEmpty(mockEventHandler.Events);
Assert.AreEqual("err", (mockEventHandler.Events[0].Extra as NetworkException).Message);
}
}
}

View File

@ -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<IManager>();
mockEventHandler = new MockEventHandler();
A.CallTo(() => fakeManager.GetEventHandler())
.Returns(mockEventHandler);
var rconConnectionFactory = serviceProvider.GetRequiredService<IRConConnectionFactory>();
A.CallTo(() => rconConnectionFactory.CreateConnection(A<string>.Ignored, A<int>.Ignored, A<string>.Ignored))
.Returns(serviceProvider.GetRequiredService<IRConConnection>());
A.CallTo(() => serviceProvider.GetRequiredService<IRConParser>().Configuration)
.Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService<IParserRegexFactory>()));
}
[Test]
public async Task Test_GenericGuidClientIsKicked()
{
var plugin = new ScriptPlugin(Path.Join(PLUGIN_DIR, "SharedGUIDKick.js"), PLUGIN_DIR);
var server = serviceProvider.GetRequiredService<IW4MServer>();
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<ClientService>();
A.CallTo(() => fakeClientService.GetUnique(A<long>.Ignored))
.Returns(Task.FromResult(databaseClient));
A.CallTo(() => fakeManager.GetClientService())
.Returns(fakeClientService);
await plugin.Initialize(serviceProvider.GetRequiredService<IManager>());
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);
}
}
}