From 161b27e2f257c11af3e5dd04f6a5fe0b317c07cb Mon Sep 17 00:00:00 2001 From: RaidMax Date: Fri, 15 Nov 2019 14:50:20 -0600 Subject: [PATCH] fix alias command sending message to origin instead of target (hopefully) fix an issue with banned players causing exception if they create events before they are kicked out fix issues with sometimes wrong error message for timeout show most recent IP address at top of alias list optimization to some sql queries --- Application/ApplicationManager.cs | 33 +- Application/EventParsers/BaseEventParser.cs | 1 + Application/GameEventHandler.cs | 23 +- Application/IO/GameLogEventDetection.cs | 6 - Application/IW4MServer.cs | 152 +-- Application/Misc/EventProfiler.cs | 63 ++ Application/Misc/Logger.cs | 16 +- Application/RconParsers/BaseRConParser.cs | 2 +- GameLogServer/requirements.txt | 2 +- Plugins/Stats/Helpers/ServerStats.cs | 8 + Plugins/Stats/Helpers/StatManager.cs | 96 +- Plugins/Stats/Plugin.cs | 2 +- Plugins/Tests/ClientTests.cs | 48 +- Plugins/Tests/PluginTests.cs | 4 +- Plugins/Tests/ServerTests.cs | 16 +- SharedLibraryCore/Commands/NativeCommands.cs | 2 +- SharedLibraryCore/Database/DatabaseContext.cs | 30 +- SharedLibraryCore/Events/GameEvent.cs | 59 +- SharedLibraryCore/Interfaces/IManager.cs | 3 +- ...rceUniqueIndexForEFAliasIPName.Designer.cs | 909 ++++++++++++++++++ ...0713_EnforceUniqueIndexForEFAliasIPName.cs | 120 +++ .../DatabaseContextModelSnapshot.cs | 7 +- SharedLibraryCore/PartialEntities/EFClient.cs | 62 +- SharedLibraryCore/ScriptPlugin.cs | 12 +- SharedLibraryCore/Server.cs | 2 +- SharedLibraryCore/Services/ClientService.cs | 123 ++- SharedLibraryCore/Services/MetaService.cs | 7 + SharedLibraryCore/SharedLibraryCore.csproj | 2 +- SharedLibraryCore/Utilities.cs | 2 +- WebfrontCore/Controllers/ClientController.cs | 18 +- .../Middleware/ClaimsPermissionRemoval.cs | 2 +- 31 files changed, 1553 insertions(+), 279 deletions(-) create mode 100644 Application/Misc/EventProfiler.cs create mode 100644 SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.cs diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index c4ff8857..988b1e1b 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -33,8 +33,6 @@ namespace IW4MAdmin.Application public ILogger Logger => GetLogger(0); public bool Running { get; private set; } public bool IsInitialized { get; private set; } - // expose the event handler so we can execute the events - public OnServerEventEventHandler OnServerEvent { get; set; } public DateTime StartTime { get; private set; } public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString(); @@ -73,21 +71,19 @@ namespace IW4MAdmin.Application PageList = new PageList(); AdditionalEventParsers = new List(); AdditionalRConParsers = new List(); - OnServerEvent += OnGameEvent; - OnServerEvent += EventApi.OnGameEvent; + //OnServerEvent += OnGameEvent; + //OnServerEvent += EventApi.OnGameEvent; TokenAuthenticator = new TokenAuthentication(); _metaService = new MetaService(); _tokenSource = new CancellationTokenSource(); } - private async void OnGameEvent(object sender, GameEventArgs args) + public async Task ExecuteEvent(GameEvent newEvent) { #if DEBUG == true - Logger.WriteDebug($"Entering event process for {args.Event.Id}"); + Logger.WriteDebug($"Entering event process for {newEvent.Id}"); #endif - var newEvent = args.Event; - // the event has failed already if (newEvent.Failed) { @@ -96,12 +92,11 @@ namespace IW4MAdmin.Application try { - await newEvent.Owner.EventProcessing.WaitAsync(CancellationToken); await newEvent.Owner.ExecuteEvent(newEvent); // save the event info to the database var changeHistorySvc = new ChangeHistoryService(); - await changeHistorySvc.Add(args.Event); + await changeHistorySvc.Add(newEvent); #if DEBUG Logger.WriteDebug($"Processed event with id {newEvent.Id}"); @@ -145,22 +140,12 @@ namespace IW4MAdmin.Application Logger.WriteDebug(ex.GetExceptionInfo()); } - finally - { - if (newEvent.Owner.EventProcessing.CurrentCount == 0) - { - newEvent.Owner.EventProcessing.Release(1); - } - -#if DEBUG == true - Logger.WriteDebug($"Exiting event process for {args.Event.Id}"); -#endif - } - skip: - // tell anyone waiting for the output that we're done - newEvent.OnProcessed.Set(); + newEvent.Complete(); +#if DEBUG == true + Logger.WriteDebug($"Exiting event process for {newEvent.Id}"); +#endif } public IList GetServers() diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index 71faa290..26ad6378 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -256,6 +256,7 @@ namespace IW4MAdmin.Application.EventParsers // this is a custom event printed out by _customcallbacks.gsc (used for anticheat) if (eventType == "ScriptKill") { + long originId = lineSplit[1].ConvertGuidToLong(1); long targetId = lineSplit[2].ConvertGuidToLong(1); diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 6b44d533..fdfb22c8 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -1,6 +1,8 @@ -using SharedLibraryCore; +using IW4MAdmin.Application.Misc; +using SharedLibraryCore; using SharedLibraryCore.Events; using SharedLibraryCore.Interfaces; +using System; using System.Linq; using System.Threading; @@ -9,7 +11,11 @@ namespace IW4MAdmin.Application class GameEventHandler : IEventHandler { readonly ApplicationManager Manager; - private static GameEvent.EventType[] overrideEvents = new[] + private readonly EventProfiler _profiler; + private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args); + private event GameEventAddedEventHandler GameEventAdded; + + private static readonly GameEvent.EventType[] overrideEvents = new[] { GameEvent.EventType.Connect, GameEvent.EventType.Disconnect, @@ -20,6 +26,17 @@ namespace IW4MAdmin.Application public GameEventHandler(IManager mgr) { Manager = (ApplicationManager)mgr; + _profiler = new EventProfiler(mgr.GetLogger(0)); + GameEventAdded += GameEventHandler_GameEventAdded; + } + + private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args) + { + var start = DateTime.Now; + await Manager.ExecuteEvent(args.Event); +#if DEBUG + _profiler.Profile(start, DateTime.Now, args.Event); +#endif } public void AddEvent(GameEvent gameEvent) @@ -35,7 +52,7 @@ namespace IW4MAdmin.Application #if DEBUG gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}"); #endif - Manager.OnServerEvent?.Invoke(gameEvent.Owner, new GameEventArgs(null, false, gameEvent)); + GameEventAdded?.Invoke(this, new GameEventArgs(null, false, gameEvent)); } #if DEBUG else diff --git a/Application/IO/GameLogEventDetection.cs b/Application/IO/GameLogEventDetection.cs index d886e81f..695080e5 100644 --- a/Application/IO/GameLogEventDetection.cs +++ b/Application/IO/GameLogEventDetection.cs @@ -76,7 +76,6 @@ namespace IW4MAdmin.Application.IO #if DEBUG _server.Logger.WriteVerbose(gameEvent.Data); #endif - // we don't want to add the event if ignoreBots is on and the event comes from a bot if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false)))) { @@ -103,11 +102,6 @@ namespace IW4MAdmin.Application.IO } _server.Manager.GetEventHandler().AddEvent(gameEvent); - - if (gameEvent.IsBlocking) - { - await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, _server.Manager.CancellationToken); - } } } diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index f070d681..1ed682c9 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -33,7 +33,7 @@ namespace IW4MAdmin { } - override public async Task OnClientConnected(EFClient clientFromLog) + override public async Task OnClientConnected(EFClient clientFromLog) { Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved"); @@ -57,6 +57,7 @@ namespace IW4MAdmin Logger.WriteInfo($"Client {client} connected..."); // Do the player specific stuff + client.ProcessingEvent = clientFromLog.ProcessingEvent; client.ClientNumber = clientFromLog.ClientNumber; client.Score = clientFromLog.Score; client.Ping = clientFromLog.Ping; @@ -73,9 +74,8 @@ namespace IW4MAdmin Type = GameEvent.EventType.Connect }; - await client.OnJoin(client.IPAddress); - client.State = ClientState.Connected; Manager.GetEventHandler().AddEvent(e); + return client; } override public async Task OnClientDisconnected(EFClient client) @@ -103,55 +103,85 @@ namespace IW4MAdmin public override async Task ExecuteEvent(GameEvent E) { - bool canExecuteCommand = true; - - if (!await ProcessEvent(E)) + if (E == null) { + Logger.WriteError("Received NULL event"); return; } - Command C = null; - if (E.Type == GameEvent.EventType.Command) + if (E.IsBlocking) { - try + await E.Origin?.Lock(); + } + + bool canExecuteCommand = true; + Exception lastException = null; + + try + { + if (!await ProcessEvent(E)) { - C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); + return; } - catch (CommandException e) + Command C = null; + if (E.Type == GameEvent.EventType.Command) { - Logger.WriteInfo(e.Message); + try + { + C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); + } + + catch (CommandException e) + { + Logger.WriteInfo(e.Message); + } + + if (C != null) + { + E.Extra = C; + } } - if (C != null) + foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) { - E.Extra = C; + try + { + await plugin.OnEventAsync(E, this); + } + catch (AuthorizationException e) + { + E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}"); + canExecuteCommand = false; + } + catch (Exception Except) + { + Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]"); + Logger.WriteDebug(Except.GetExceptionInfo()); + } + } + + // hack: this prevents commands from getting executing that 'shouldn't' be + if (E.Type == GameEvent.EventType.Command && E.Extra is Command command && + (canExecuteCommand || E.Origin?.Level == Permission.Console)) + { + await command.ExecuteAsync(E); } } - foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) + catch (Exception e) { - try - { - await plugin.OnEventAsync(E, this); - } - catch (AuthorizationException e) - { - E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}"); - canExecuteCommand = false; - } - catch (Exception Except) - { - Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]"); - Logger.WriteDebug(Except.GetExceptionInfo()); - } + lastException = e; } - // hack: this prevents commands from getting executing that 'shouldn't' be - if (E.Type == GameEvent.EventType.Command && E.Extra is Command command && - (canExecuteCommand || E.Origin?.Level == Permission.Console)) + finally { - await command.ExecuteAsync(E); + E.Origin?.Unlock(); + + if (lastException != null) + { + throw lastException; + } } } @@ -195,6 +225,25 @@ namespace IW4MAdmin await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin); } + else if (E.Type == GameEvent.EventType.Connect) + { + if (E.Origin.State != ClientState.Connected) + { + E.Origin.State = ClientState.Connected; + E.Origin.LastConnection = DateTime.UtcNow; + E.Origin.Connections += 1; + + ChatHistory.Add(new ChatInfo() + { + Name = E.Origin.Name, + Message = "CONNECTED", + Time = DateTime.UtcNow + }); + + await E.Origin.OnJoin(E.Origin.IPAddress); + } + } + else if (E.Type == GameEvent.EventType.PreConnect) { // we don't want to track bots in the database at all if ignore bots is requested @@ -230,7 +279,8 @@ namespace IW4MAdmin Clients[E.Origin.ClientNumber] = E.Origin; try { - await OnClientConnected(E.Origin); + E.Origin = await OnClientConnected(E.Origin); + E.Target = E.Origin; } catch (Exception ex) @@ -242,13 +292,6 @@ namespace IW4MAdmin return false; } - ChatHistory.Add(new ChatInfo() - { - Name = E.Origin.Name, - Message = "CONNECTED", - Time = DateTime.UtcNow - }); - if (E.Origin.Level > EFClient.Permission.Moderator) { E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); @@ -624,7 +667,6 @@ namespace IW4MAdmin #endif var polledClients = await PollPlayersAsync(); - var waiterList = new List(); foreach (var disconnectingClient in polledClients[1]) { @@ -641,18 +683,9 @@ namespace IW4MAdmin }; Manager.GetEventHandler().AddEvent(e); - // wait until the disconnect event is complete - // because we don't want to try to fill up a slot that's not empty yet - waiterList.Add(e); + await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); } - // wait for all the disconnect tasks to finish - foreach (var waiter in waiterList) - { - waiter.Wait(); - } - - waiterList.Clear(); // this are our new connecting clients foreach (var client in polledClients[0]) { @@ -671,16 +704,9 @@ namespace IW4MAdmin }; Manager.GetEventHandler().AddEvent(e); - waiterList.Add(e); + await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); } - // wait for all the connect tasks to finish - foreach (var waiter in waiterList) - { - waiter.Wait(); - } - - waiterList.Clear(); // these are the clients that have updated foreach (var client in polledClients[2]) { @@ -692,12 +718,6 @@ namespace IW4MAdmin }; Manager.GetEventHandler().AddEvent(e); - waiterList.Add(e); - } - - foreach (var waiter in waiterList) - { - waiter.Wait(); } if (ConnectionErrors > 0) diff --git a/Application/Misc/EventProfiler.cs b/Application/Misc/EventProfiler.cs new file mode 100644 index 00000000..0b884f67 --- /dev/null +++ b/Application/Misc/EventProfiler.cs @@ -0,0 +1,63 @@ +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IW4MAdmin.Application.Misc +{ + internal class EventPerformance + { + public long ExecutionTime { get; set; } + public GameEvent Event { get; set; } + public string EventInfo => $"{Event.Type}, {Event.FailReason}, {Event.IsBlocking}, {Event.Data}, {Event.Message}, {Event.Extra}"; + } + + public class DuplicateKeyComparer : IComparer where TKey : IComparable + { + public int Compare(TKey x, TKey y) + { + int result = x.CompareTo(y); + + if (result == 0) + return 1; + else + return result; + } + } + + internal class EventProfiler + { + public double AverageEventTime { get; private set; } + public double MaxEventTime => Events.Values.Last().ExecutionTime; + public double MinEventTime => Events.Values[0].ExecutionTime; + public int TotalEventCount => Events.Count; + public SortedList Events { get; private set; } = new SortedList(new DuplicateKeyComparer()); + private readonly ILogger _logger; + + public EventProfiler(ILogger logger) + { + _logger = logger; + } + + public void Profile(DateTime start, DateTime end, GameEvent gameEvent) + { + _logger.WriteDebug($"Starting profile of event {gameEvent.Id}"); + long executionTime = (long)Math.Round((end - start).TotalMilliseconds); + + var perf = new EventPerformance() + { + Event = gameEvent, + ExecutionTime = executionTime + }; + + lock (Events) + { + Events.Add(executionTime, perf); + } + + AverageEventTime = (AverageEventTime * (TotalEventCount - 1) + executionTime) / TotalEventCount; + _logger.WriteDebug($"Finished profile of event {gameEvent.Id}"); + } + } +} diff --git a/Application/Misc/Logger.cs b/Application/Misc/Logger.cs index 0f889043..e1962db8 100644 --- a/Application/Misc/Logger.cs +++ b/Application/Misc/Logger.cs @@ -3,7 +3,6 @@ using SharedLibraryCore.Interfaces; using System; using System.IO; using System.Threading; -using System.Threading.Tasks; namespace IW4MAdmin.Application { @@ -20,16 +19,21 @@ namespace IW4MAdmin.Application } readonly string FileName; - readonly SemaphoreSlim OnLogWriting; + readonly ReaderWriterLockSlim WritingLock; static readonly short MAX_LOG_FILES = 10; public Logger(string fn) { FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log"); - OnLogWriting = new SemaphoreSlim(1, 1); + WritingLock = new ReaderWriterLockSlim(); RotateLogs(); } + ~Logger() + { + WritingLock.Dispose(); + } + /// /// rotates logs when log is initialized /// @@ -56,7 +60,7 @@ namespace IW4MAdmin.Application void Write(string msg, LogType type) { - OnLogWriting.Wait(); + WritingLock.EnterWriteLock(); string stringType = type.ToString(); msg = msg.StripColors(); @@ -74,7 +78,7 @@ namespace IW4MAdmin.Application #if DEBUG // lets keep it simple and dispose of everything quickly as logging wont be that much (relatively) Console.WriteLine(LogLine); - File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}"); + //File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}"); //Debug.WriteLine(msg); #else if (type == LogType.Error || type == LogType.Verbose) @@ -91,7 +95,7 @@ namespace IW4MAdmin.Application Console.WriteLine(ex.GetExceptionInfo()); } - OnLogWriting.Release(1); + WritingLock.ExitWriteLock(); } public void WriteVerbose(string msg) diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RconParsers/BaseRConParser.cs index 14d6ca65..4ca038ec 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RconParsers/BaseRConParser.cs @@ -38,7 +38,7 @@ namespace IW4MAdmin.Application.RconParsers }, }; - Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$"; + Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback|unknown) +(-*[0-9]+) +([0-9]+) *$"; Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1); Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2); Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3); diff --git a/GameLogServer/requirements.txt b/GameLogServer/requirements.txt index 3ee54e69..03611697 100644 --- a/GameLogServer/requirements.txt +++ b/GameLogServer/requirements.txt @@ -9,4 +9,4 @@ pip==10.0.1 pytz==2018.9 setuptools==39.0.1 six==1.12.0 -Werkzeug==0.15.2 +Werkzeug==0.16.0 diff --git a/Plugins/Stats/Helpers/ServerStats.cs b/Plugins/Stats/Helpers/ServerStats.cs index 73b44bc3..e1c9af66 100644 --- a/Plugins/Stats/Helpers/ServerStats.cs +++ b/Plugins/Stats/Helpers/ServerStats.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace IW4MAdmin.Plugins.Stats.Helpers { @@ -16,6 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public EFServer Server { get; private set; } private readonly Server _server; public bool IsTeamBased { get; set; } + public SemaphoreSlim OnSaving { get; private set; } public ServerStats(EFServer sv, EFServerStatistics st, Server server) { @@ -23,6 +25,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers ServerStatistics = st; Server = sv; _server = server; + OnSaving = new SemaphoreSlim(1, 1); + } + + ~ServerStats() + { + OnSaving.Dispose(); } public int TeamCount(IW4Info.Team teamName) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 8f1e9298..0943ea46 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -410,6 +410,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Vector3 vDeathOrigin = null; Vector3 vKillOrigin = null; Vector3 vViewAngles = null; + var snapshotAngles = new List(); SemaphoreSlim waiter = null; try @@ -419,20 +420,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers vDeathOrigin = Vector3.Parse(deathOrigin); vKillOrigin = Vector3.Parse(killOrigin); vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(); - } - catch (FormatException) - { - _log.WriteWarning("Could not parse kill or death origin or viewangle vectors"); - _log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}"); - await AddStandardKill(attacker, victim); - return; - } - - var snapshotAngles = new List(); - - try - { foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries)) { snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles()); @@ -441,7 +429,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers catch (FormatException) { - _log.WriteWarning("Could not parse snapshot angles"); + _log.WriteError("Could not parse vector data from hit"); + _log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles} Snapshot - {string.Join(",", snapshotAngles.Select(_a => _a.ToString()))}"); return; } @@ -478,7 +467,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var clientStats = attacker.GetAdditionalProperty(CLIENT_STATS_KEY); waiter = clientStats.ProcessingHit; - await waiter.WaitAsync(); + await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken); // increment their hit count if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || @@ -495,12 +484,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (Plugin.Config.Configuration().StoreClientKills) { - var cache = _servers[serverId].HitCache; - cache.Add(hit); - - if (cache.Count > MAX_CACHED_HITS) + var serverWaiter = _servers[serverId].OnSaving; + try { - await SaveHitCache(serverId); + await serverWaiter.WaitAsync(); + var cache = _servers[serverId].HitCache; + cache.Add(hit); + + if (cache.Count > MAX_CACHED_HITS) + { + await SaveHitCache(serverId); + } + } + + catch (Exception e) + { + _log.WriteError("Could not store client kills"); + _log.WriteDebug(e.GetExceptionInfo()); + } + + finally + { + if (serverWaiter.CurrentCount == 0) + { + serverWaiter.Release(1); + } } } @@ -538,9 +546,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } } -#if DEBUG - await Sync(attacker.CurrentServer); -#endif } catch (Exception ex) @@ -552,7 +557,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers finally { - waiter?.Release(1); + if (waiter?.CurrentCount == 0) + { + waiter.Release(); + } } } @@ -561,7 +569,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers using (var ctx = new DatabaseContext(true)) { var server = _servers[serverId]; - ctx.AddRange(server.HitCache); + ctx.AddRange(server.HitCache.ToList()); await ctx.SaveChangesAsync(); server.HitCache.Clear(); } @@ -1110,21 +1118,41 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { long serverId = GetIdForServer(sv); - using (var ctx = new DatabaseContext()) + var waiter = _servers[serverId].OnSaving; + try { - var serverStatsSet = ctx.Set(); - serverStatsSet.Update(_servers[serverId].ServerStatistics); - await ctx.SaveChangesAsync(); + await waiter.WaitAsync(); + + using (var ctx = new DatabaseContext()) + { + var serverStatsSet = ctx.Set(); + serverStatsSet.Update(_servers[serverId].ServerStatistics); + await ctx.SaveChangesAsync(); + } + + foreach (var stats in sv.GetClientsAsList() + .Select(_client => _client.GetAdditionalProperty(CLIENT_STATS_KEY)) + .Where(_stats => _stats != null)) + { + await SaveClientStats(stats); + } + + await SaveHitCache(serverId); } - foreach (var stats in sv.GetClientsAsList() - .Select(_client => _client.GetAdditionalProperty(CLIENT_STATS_KEY)) - .Where(_stats => _stats != null)) + catch (Exception e) { - await SaveClientStats(stats); + _log.WriteError("There was a probably syncing server stats"); + _log.WriteDebug(e.GetExceptionInfo()); } - await SaveHitCache(serverId); + finally + { + if (waiter.CurrentCount == 0) + { + waiter.Release(1); + } + } } public void SetTeamBased(long serverId, bool isTeamBased) diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index c093b427..bd0604ad 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -43,7 +43,7 @@ namespace IW4MAdmin.Plugins.Stats break; case GameEvent.EventType.Stop: break; - case GameEvent.EventType.Connect: + case GameEvent.EventType.PreConnect: await Manager.AddPlayer(E.Origin); break; case GameEvent.EventType.Disconnect: diff --git a/Plugins/Tests/ClientTests.cs b/Plugins/Tests/ClientTests.cs index 597a3bcb..92da5a04 100644 --- a/Plugins/Tests/ClientTests.cs +++ b/Plugins/Tests/ClientTests.cs @@ -73,7 +73,7 @@ namespace Tests }; _manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.Complete(); e = new GameEvent() { @@ -92,7 +92,7 @@ namespace Tests }; _manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.Complete(); e = new GameEvent() { @@ -111,7 +111,7 @@ namespace Tests }; _manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.Complete(); } @@ -126,13 +126,13 @@ namespace Tests Thread.Sleep(100); } - _manager.OnServerEvent += (sender, eventArgs) => - { - if (eventArgs.Event.Type == GameEvent.EventType.Connect) - { - onJoined.Set(); - } - }; + //_manager.OnServerEvent += (sender, eventArgs) => + //{ + // if (eventArgs.Event.Type == GameEvent.EventType.Connect) + // { + // onJoined.Set(); + // } + //}; server.EmulateClientJoinLog(); onJoined.Wait(); @@ -140,25 +140,25 @@ namespace Tests var client = server.Clients[0]; var warnEvent = client.Warn("test warn", Utilities.IW4MAdminClient(server)); - warnEvent.OnProcessed.Wait(5000); + warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.False(warnEvent.Failed); warnEvent = client.Warn("test warn", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer }); - warnEvent.OnProcessed.Wait(5000); + warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.True(warnEvent.FailReason == GameEvent.EventFailReason.Permission && client.Warnings == 1, "warning was applied without proper permissions"); // warn clear var warnClearEvent = client.WarnClear(new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer }); - warnClearEvent.OnProcessed.Wait(5000); + warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.True(warnClearEvent.FailReason == GameEvent.EventFailReason.Permission && client.Warnings == 1, "warning was removed without proper permissions"); warnClearEvent = client.WarnClear(Utilities.IW4MAdminClient(server)); - warnClearEvent.OnProcessed.Wait(5000); + warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.True(!warnClearEvent.Failed && client.Warnings == 0, "warning was not cleared"); } @@ -178,14 +178,14 @@ namespace Tests var player = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }; player.SetAdditionalProperty("_reportCount", 3); var reportEvent = client.Report("test report", player); - reportEvent.OnProcessed.Wait(TestTimeout); + reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Throttle & client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 0, $"too many reports were applied [{reportEvent.FailReason.ToString()}]"); // succeed reportEvent = client.Report("test report", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - reportEvent.OnProcessed.Wait(TestTimeout); + reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait(); Assert.True(!reportEvent.Failed && client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was not applied [{reportEvent.FailReason.ToString()}]"); @@ -222,7 +222,7 @@ namespace Tests Assert.False(client == null, "no client found to flag"); var flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - flagEvent.OnProcessed.Wait(); + flagEvent.Complete(); // succeed Assert.True(!flagEvent.Failed && @@ -230,31 +230,31 @@ namespace Tests Assert.False(client.ReceivedPenalties.FirstOrDefault(p => p.Offense == "test flag") == null, "flag was not applied"); flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer }); - flagEvent.OnProcessed.Wait(); + flagEvent.Complete(); // fail Assert.True(client.ReceivedPenalties.Count == 1, "flag was applied without permisions"); flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - flagEvent.OnProcessed.Wait(); + flagEvent.Complete(); // fail Assert.True(client.ReceivedPenalties.Count == 1, "duplicate flag was applied"); var unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer }); - unflagEvent.OnProcessed.Wait(); + unflagEvent.Complete(); // fail Assert.False(client.Level == EFClient.Permission.User, "user was unflagged without permissions"); unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - unflagEvent.OnProcessed.Wait(); + unflagEvent.Complete(); // succeed Assert.True(client.Level == EFClient.Permission.User, "user was not unflagged"); unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - unflagEvent.OnProcessed.Wait(); + unflagEvent.Complete(); // succeed Assert.True(unflagEvent.FailReason == GameEvent.EventFailReason.Invalid, "user was not flagged"); @@ -272,12 +272,12 @@ namespace Tests Assert.False(client == null, "no client found to kick"); var kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer }); - kickEvent.OnProcessed.Wait(); + kickEvent.Complete(); Assert.True(kickEvent.FailReason == GameEvent.EventFailReason.Permission, "client was kicked without permission"); kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer }); - kickEvent.OnProcessed.Wait(); + kickEvent.Complete(); Assert.True(_manager.Servers.First().GetClientsAsList().FirstOrDefault(c => c.NetworkId == client.NetworkId) == null, "client was not kicked"); } diff --git a/Plugins/Tests/PluginTests.cs b/Plugins/Tests/PluginTests.cs index 8f4f4a1c..98046714 100644 --- a/Plugins/Tests/PluginTests.cs +++ b/Plugins/Tests/PluginTests.cs @@ -31,7 +31,7 @@ namespace Tests }; Manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.Complete(); var client = Manager.GetServers()[0].Clients[0]; @@ -44,7 +44,7 @@ namespace Tests }; Manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.Complete(); Assert.True(client.Warnings == 1, "client wasn't warned for objectional language"); } diff --git a/Plugins/Tests/ServerTests.cs b/Plugins/Tests/ServerTests.cs index 69c65932..9f0a984a 100644 --- a/Plugins/Tests/ServerTests.cs +++ b/Plugins/Tests/ServerTests.cs @@ -28,11 +28,11 @@ namespace Tests var currentClientCount = server.ClientNum; int eventsProcessed = 0; - _manager.OnServerEvent += (sender, eventArgs) => + /*_manager.OnServerEvent += (sender, eventArgs) => { if (eventArgs.Event.Type == GameEvent.EventType.Connect) { - eventArgs.Event.OnProcessed.Wait(); + eventArgs.Event.Complete(); Assert.False(eventArgs.Event.Failed, "connect event was not processed"); Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented"); eventsProcessed++; @@ -41,13 +41,13 @@ namespace Tests if (eventArgs.Event.Type == GameEvent.EventType.Disconnect) { - eventArgs.Event.OnProcessed.Wait(); + eventArgs.Event.Complete(); Assert.False(eventArgs.Event.Failed, "disconnect event was not processed"); Assert.True(server.ClientNum == currentClientCount, "client count was not decremented"); eventsProcessed++; resetEvent.Set(); } - }; + };*/ server.EmulateClientJoinLog(); @@ -73,11 +73,11 @@ namespace Tests int eventsProcessed = 0; _manager.GetApplicationSettings().Configuration().RConPollRate = 5000; - _manager.OnServerEvent += (sender, eventArgs) => + /*_manager.OnServerEvent += (sender, eventArgs) => { if (eventArgs.Event.Type == GameEvent.EventType.Connect) { - eventArgs.Event.OnProcessed.Wait(); + eventArgs.Event.Complete(); Assert.False(eventArgs.Event.Failed, "connect event was not processed"); Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented"); eventsProcessed++; @@ -86,13 +86,13 @@ namespace Tests if (eventArgs.Event.Type == GameEvent.EventType.Disconnect) { - eventArgs.Event.OnProcessed.Wait(); + eventArgs.Event.Complete(); Assert.False(eventArgs.Event.Failed, "disconnect event was not processed"); Assert.True(server.ClientNum == currentClientCount, "client count was not decremented"); eventsProcessed++; resetEvent.Set(); } - }; + };*/ (server.RconParser as TestRconParser).FakeClientCount = 1; diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 2f763ee3..d51a5101 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -998,7 +998,7 @@ namespace SharedLibraryCore.Commands var names = new List(E.Target.AliasLink.Children.Select(a => a.Name)); var IPs = new List(E.Target.AliasLink.Children.Select(a => a.IPAddress.ConvertIPtoString()).Distinct()); - E.Target.Tell($"[^3{E.Target}^7]"); + E.Origin.Tell($"[^3{E.Target}^7]"); message.Append($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ALIAS_ALIASES"]}: "); message.Append(String.Join(" | ", names)); diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs index 58a8d89f..66a56875 100644 --- a/SharedLibraryCore/Database/DatabaseContext.cs +++ b/SharedLibraryCore/Database/DatabaseContext.cs @@ -22,40 +22,26 @@ namespace SharedLibraryCore.Database public DbSet EFMeta { get; set; } public DbSet EFChangeHistory { get; set; } - - //[Obsolete] - //private static readonly ILoggerFactory _loggerFactory = new LoggerFactory(new[] { - // new ConsoleLoggerProvider((category, level) => level == LogLevel.Information, true) - //}); - static string _ConnectionString; static string _provider; private static readonly string _migrationPluginDirectory = @"X:\IW4MAdmin\BUILD\Plugins"; - private static int activeContextCount; + private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole() + .AddDebug() + .AddFilter((category, level) => true); + }); public DatabaseContext(DbContextOptions opt) : base(opt) { -#if DEBUG == true - activeContextCount++; - //Console.WriteLine($"Initialized DB Context #{activeContextCount}"); -#endif } public DatabaseContext() { -#if DEBUG == true - activeContextCount++; - //Console.WriteLine($"Initialized DB Context #{activeContextCount}"); -#endif } public override void Dispose() { -#if DEBUG == true - - //Console.WriteLine($"Disposed DB Context #{activeContextCount}"); - activeContextCount--; -#endif } public DatabaseContext(bool disableTracking) : this() @@ -83,6 +69,9 @@ namespace SharedLibraryCore.Database protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + // optionsBuilder.UseLoggerFactory(_loggerFactory) + // .EnableSensitiveDataLogging(); + if (string.IsNullOrEmpty(_ConnectionString)) { string currentPath = Utilities.OperatingDirectory; @@ -153,6 +142,7 @@ namespace SharedLibraryCore.Database ent.HasIndex(a => a.Name); ent.Property(_alias => _alias.SearchableName).HasMaxLength(24); ent.HasIndex(_alias => _alias.SearchableName); + ent.HasIndex(_alias => new { _alias.Name, _alias.IPAddress }).IsUnique(); }); modelBuilder.Entity(ent => diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs index 846a00de..27ad276c 100644 --- a/SharedLibraryCore/Events/GameEvent.cs +++ b/SharedLibraryCore/Events/GameEvent.cs @@ -8,9 +8,6 @@ namespace SharedLibraryCore { public class GameEvent { - // define what the delagate function looks like - public delegate void OnServerEventEventHandler(object sender, GameEventArgs e); - public enum EventFailReason { /// @@ -205,11 +202,17 @@ namespace SharedLibraryCore public GameEvent() { - OnProcessed = new ManualResetEventSlim(false); + _eventFinishedWaiter = new ManualResetEvent(false); Time = DateTime.UtcNow; Id = GetNextEventId(); } + ~GameEvent() + { + _eventFinishedWaiter.Set(); + _eventFinishedWaiter.Dispose(); + } + public EventType Type; public EventRequiredEntity RequiredEntity { get; set; } public string Data; // Data is usually the message sent by player @@ -219,34 +222,56 @@ namespace SharedLibraryCore public Server Owner; public bool IsRemote { get; set; } = false; public object Extra { get; set; } - public ManualResetEventSlim OnProcessed { get; set; } + private readonly ManualResetEvent _eventFinishedWaiter; public DateTime Time { get; set; } public long Id { get; private set; } public EventFailReason FailReason { get; set; } public bool Failed => FailReason != EventFailReason.None; + /// /// Indicates if the event should block until it is complete /// public bool IsBlocking { get; set; } + public void Complete() + { + _eventFinishedWaiter.Set(); +#if DEBUG + Owner?.Logger.WriteDebug($"Completed internal for event {Id}"); +#endif + } + /// /// asynchronously wait for GameEvent to be processed /// /// waitable task - public Task WaitAsync(TimeSpan timeSpan, CancellationToken token) + public async Task WaitAsync(TimeSpan timeSpan, CancellationToken token) { - return Task.Run(() => - { - bool processed = OnProcessed.Wait(timeSpan, token); - // this let's us know if the the action timed out - FailReason = FailReason == EventFailReason.None & !processed ? EventFailReason.Timeout : FailReason; - return this; - }); - } + bool processed = false; - public GameEvent Wait() - { - OnProcessed.Wait(); +#if DEBUG + Owner?.Logger.WriteDebug($"Begin wait for event {Id}"); +#endif + + try + { + processed = await Task.Run(() => _eventFinishedWaiter.WaitOne(timeSpan), token); + } + catch { } + + + if (!processed) + { +#if DEBUG + //throw new Exception(); +#endif + Owner?.Logger.WriteError("Waiting for event to complete timed out"); + Owner?.Logger.WriteDebug($"{Id}, {Type}, {Data}, {Extra}, {FailReason.ToString()}, {Message}, {Origin}, {Target}"); + } + + + // this lets us know if the the action timed out + FailReason = FailReason == EventFailReason.None && !processed ? EventFailReason.Timeout : FailReason; return this; } } diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 8d48b72f..dbd0167c 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -55,6 +55,7 @@ namespace SharedLibraryCore.Interfaces string ExternalIPAddress { get; } CancellationToken CancellationToken { get; } bool IsRestartRequested { get; } - OnServerEventEventHandler OnServerEvent { get; set; } + //OnServerEventEventHandler OnServerEvent { get; set; } + Task ExecuteEvent(GameEvent gameEvent); } } diff --git a/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.Designer.cs b/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.Designer.cs new file mode 100644 index 00000000..67e9bc33 --- /dev/null +++ b/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.Designer.cs @@ -0,0 +1,909 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20191030000713_EnforceUniqueIndexForEFAliasIPName")] + partial class EnforceUniqueIndexForEFAliasIPName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageRecoilOffset") + .HasColumnType("REAL"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("VisionAverage") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnName("EFClientStatisticsClientId") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsServerId") + .HasColumnName("EFClientStatisticsServerId") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(24); + + b.Property("SearchableName") + .HasColumnType("TEXT") + .HasMaxLength(24); + + b.HasKey("AliasId"); + + b.HasIndex("LinkId"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress") + .IsUnique(); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.cs b/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.cs new file mode 100644 index 00000000..a3d604e9 --- /dev/null +++ b/SharedLibraryCore/Migrations/20191030000713_EnforceUniqueIndexForEFAliasIPName.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class EnforceUniqueIndexForEFAliasIPName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder.ActiveProvider == "Microsoft.EntityFrameworkCore.Sqlite") + { + migrationBuilder.Sql(@"DELETE FROM EFAlias WHERE AliasId IN ( + SELECT AliasId from ( + SELECT AliasId, Name, Min(DateAdded), IPAddress FROM EFAlias where (IPAddress, Name) in (SELECT DISTINCT IPAddress, Name FROM EFAlias GROUP BY EFAlias.IPAddress, Name HAVING count(IPAddress) > 1 AND count(Name) > 1) + GROUP BY IPAddress ORDER BY IPAddress))", true); + migrationBuilder.Sql(@"CREATE UNIQUE INDEX IX_EFAlias_Name_IPAddress ON EFAlias ( IPAddress, Name );", true); + return; + } + + else if (migrationBuilder.ActiveProvider == "Pomelo.EntityFrameworkCore.MySql") + { + migrationBuilder.Sql(@"CREATE TEMPORARY TABLE DUPLICATE_ALIASES +SELECT + MIN(`AliasId`) `MIN`, + MAX(`AliasId`) `MAX`, + `LinkId` +FROM + `EFAlias` +WHERE + (`IPAddress`, `NAME`) IN( + SELECT DISTINCT + `IPAddress`, + `NAME` + FROM + `EFAlias` + GROUP BY + `EFAlias`.`IPAddress`, + `NAME` + HAVING + COUNT(`IPAddress`) > 1 AND COUNT(`NAME`) > 1 +) +GROUP BY + `IPAddress` +ORDER BY + `IPAddress`; +SET + SQL_SAFE_UPDATES = 0; +UPDATE + `EFClients` AS `Client` +JOIN + DUPLICATE_ALIASES `Duplicate` +ON + `Client`.CurrentAliasId = `Duplicate`.`MIN` +SET + `Client`.CurrentAliasId = `Duplicate`.`MAX` +WHERE + `Client`.`CurrentAliasId` IN( + SELECT + `MIN` + FROM + DUPLICATE_ALIASES +); +DELETE +FROM + `EFAlias` +WHERE + `AliasId` IN( + SELECT + `MIN` + FROM + DUPLICATE_ALIASES +); +SET + SQL_SAFE_UPDATES = 1; +DROP TABLE + DUPLICATE_ALIASES;"); + } + + else + { + migrationBuilder.Sql(@"DELETE +FROM ""EFAlias"" +WHERE ""AliasId"" +IN +( + SELECT MIN(""AliasId"") AliasId + + FROM ""EFAlias"" WHERE(""IPAddress"", ""Name"") + + IN + ( + SELECT DISTINCT ""IPAddress"", ""Name"" + + FROM ""EFAlias"" + + GROUP BY ""EFAlias"".""IPAddress"", ""Name"" + + HAVING COUNT(""IPAddress"") > 1 AND COUNT(""Name"") > 1 + ) + + GROUP BY ""IPAddress"" + + ORDER BY ""IPAddress"" +)", true); + } + + migrationBuilder.CreateIndex( + name: "IX_EFAlias_Name_IPAddress", + table: "EFAlias", + columns: new[] { "Name", "IPAddress" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFAlias_Name_IPAddress", + table: "EFAlias"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 066db2b6..6350362c 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -464,14 +464,13 @@ namespace SharedLibraryCore.Migrations b.HasKey("AliasId"); - b.HasIndex("IPAddress"); - b.HasIndex("LinkId"); - b.HasIndex("Name"); - b.HasIndex("SearchableName"); + b.HasIndex("Name", "IPAddress") + .IsUnique(); + b.ToTable("EFAlias"); }); diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index d779c1ae..587b7eb7 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace SharedLibraryCore.Database.Models @@ -82,6 +82,12 @@ namespace SharedLibraryCore.Database.Models { "_reportCount", 0 } }; ReceivedPenalties = new List(); + ProcessingEvent = new SemaphoreSlim(1, 1); + } + + ~EFClient() + { + ProcessingEvent.Dispose(); } public override string ToString() @@ -108,6 +114,7 @@ namespace SharedLibraryCore.Database.Models [NotMapped] public string IPAddressString => IPAddress.ConvertIPtoString(); + [NotMapped] public virtual IDictionary LinkedAccounts { get; set; } @@ -452,13 +459,10 @@ namespace SharedLibraryCore.Database.Models /// /// Handles any client related logic on connection /// - public bool OnConnect() + public bool IsAbleToConnectSimple() { var loc = Utilities.CurrentLocalization.LocalizationIndex; - LastConnection = DateTime.UtcNow; - Connections += 1; - string strippedName = Name.StripColors(); if (string.IsNullOrWhiteSpace(Name) || strippedName.Replace(" ", "").Length < 3) { @@ -524,27 +528,29 @@ namespace SharedLibraryCore.Database.Models { IPAddress = ipAddress; await CurrentServer.Manager.GetClientService().UpdateAlias(this); + CurrentServer.Logger.WriteDebug($"Updated alias for {this}"); await CurrentServer.Manager.GetClientService().Update(this); + CurrentServer.Logger.WriteDebug($"Updated client for {this}"); bool canConnect = await CanConnect(ipAddress); - if (canConnect) + if (!canConnect) + { + CurrentServer.Logger.WriteDebug($"Client {this} is not allowed to join the server"); + } + + else { var e = new GameEvent() { Type = GameEvent.EventType.Join, Origin = this, Target = this, - Owner = CurrentServer + Owner = CurrentServer, }; CurrentServer.Manager.GetEventHandler().AddEvent(e); } - - else - { - CurrentServer.Logger.WriteDebug($"Client {this} is not allowed to join the server"); - } } else @@ -560,6 +566,13 @@ namespace SharedLibraryCore.Database.Models var loc = Utilities.CurrentLocalization.LocalizationIndex; var autoKickClient = Utilities.IW4MAdminClient(CurrentServer); + bool isAbleToConnectSimple = IsAbleToConnectSimple(); + + if (!isAbleToConnectSimple) + { + return false; + } + // we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID) var activePenalties = await CurrentServer.Manager.GetPenaltyService().GetActivePenaltiesAsync(AliasLinkId, ipAddress); @@ -609,7 +622,7 @@ namespace SharedLibraryCore.Database.Models Unflag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTOFLAG_UNFLAG"], autoKickClient); } - return OnConnect(); + return true; } [NotMapped] @@ -661,6 +674,29 @@ namespace SharedLibraryCore.Database.Models .LocalizationIndex[$"GLOBAL_PERMISSION_{Level.ToString().ToUpper()}"] }; + [NotMapped] + public SemaphoreSlim ProcessingEvent; + + public async Task Lock() + { + bool result = await ProcessingEvent.WaitAsync(Utilities.DefaultCommandTimeout); + +#if DEBUG + if (!result) + { + throw new InvalidOperationException(); + } +#endif + } + + public void Unlock() + { + if (ProcessingEvent.CurrentCount == 0) + { + ProcessingEvent.Release(1); + } + } + public override bool Equals(object obj) { return ((EFClient)obj).NetworkId == this.NetworkId; diff --git a/SharedLibraryCore/ScriptPlugin.cs b/SharedLibraryCore/ScriptPlugin.cs index aecebd39..92cd83ef 100644 --- a/SharedLibraryCore/ScriptPlugin.cs +++ b/SharedLibraryCore/ScriptPlugin.cs @@ -19,19 +19,25 @@ namespace SharedLibraryCore private Jint.Engine ScriptEngine; private readonly string FileName; private IManager Manager; + private readonly FileSystemWatcher _watcher; public ScriptPlugin(string fileName) { FileName = fileName; - var watcher = new FileSystemWatcher() + _watcher = new FileSystemWatcher() { Path = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", NotifyFilter = NotifyFilters.Size, Filter = fileName.Split(Path.DirectorySeparatorChar).Last() }; - watcher.Changed += Watcher_Changed; - watcher.EnableRaisingEvents = true; + _watcher.Changed += Watcher_Changed; + _watcher.EnableRaisingEvents = true; + } + + ~ScriptPlugin() + { + _watcher.Dispose(); } private async void Watcher_Changed(object sender, FileSystemEventArgs e) diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 440291a8..5cea93f7 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -67,7 +67,7 @@ namespace SharedLibraryCore /// /// EFClient pulled from memory reading /// True if player added sucessfully, false otherwise - public abstract Task OnClientConnected(EFClient P); + public abstract Task OnClientConnected(EFClient P); /// /// Remove player by client number diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index 795294df..40e33666 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -21,19 +21,23 @@ namespace SharedLibraryCore.Services if (entity.IPAddress != null) { - var existingAlias = await context.Aliases + var existingAliases = await context.Aliases .Select(_alias => new { _alias.AliasId, _alias.LinkId, _alias.IPAddress, _alias.Name }) - .FirstOrDefaultAsync(_alias => _alias.IPAddress == entity.IPAddress); + .Where(_alias => _alias.IPAddress == entity.IPAddress) + .ToListAsync(); - if (existingAlias != null) + if (existingAliases.Count > 0) { - entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing link {existingAlias.LinkId}"); + linkId = existingAliases.First().LinkId; - linkId = existingAlias.LinkId; - if (existingAlias.Name == entity.Name) + entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing link {linkId}"); + + var existingExactAlias = existingAliases.FirstOrDefault(_alias => _alias.Name == entity.Name); + + if (existingExactAlias != null) { - entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing alias {existingAlias.AliasId}"); - aliasId = existingAlias.AliasId; + entity.CurrentServer.Logger.WriteDebug($"[create] client with new GUID {entity} has existing alias {existingExactAlias.AliasId}"); + aliasId = existingExactAlias.AliasId; } } } @@ -99,6 +103,8 @@ namespace SharedLibraryCore.Services private async Task UpdateAlias(string name, int? ip, EFClient entity, DatabaseContext context) { + entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"Begin update alias for {entity}"); + // entity is the tracked db context item // get all aliases by IP address and LinkId var iqAliases = context.Aliases @@ -106,11 +112,33 @@ namespace SharedLibraryCore.Services // we only want alias that have the same IP address or share a link .Where(_alias => _alias.IPAddress == ip || (_alias.LinkId == entity.AliasLinkId)); -#if DEBUG == true - var aliasSql = iqAliases.ToSql(); -#endif var aliases = await iqAliases.ToListAsync(); + //// update each of the aliases where this is no IP but the name is identical + //foreach (var alias in aliases.Where(_alias => (_alias.IPAddress == null || _alias.IPAddress == 0))) + //{ + // alias.IPAddress = ip; + //} + + //// remove any possible duplicates after updating + //foreach (var aliasGroup in aliases.GroupBy(_alias => new { _alias.IPAddress, _alias.Name }) + // .Where(_group => _group.Count() > 1)) + //{ + // var oldestDuplicateAlias = aliasGroup.OrderBy(_alias => _alias.DateAdded).First(); + + // entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"Oldest duplicate is {oldestDuplicateAlias.AliasId}"); + + // await context.Clients.Where(_client => aliasGroup.Select(_grp => _grp.AliasId).Contains(_client.CurrentAliasId)) + // .ForEachAsync(_client => _client.CurrentAliasId = oldestDuplicateAlias.AliasId); + + // var duplicateAliases = aliasGroup.Where(_alias => _alias.AliasId != oldestDuplicateAlias.AliasId); + // context.RemoveRange(duplicateAliases); + + // await context.SaveChangesAsync(); + + // entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"Removed duplicate aliases {string.Join(",", duplicateAliases.Select(_alias => _alias.AliasId))}"); + //} + // see if they have a matching IP + Name but new NetworkId var existingExactAlias = aliases.FirstOrDefault(a => a.Name == name && a.IPAddress == ip); bool hasExactAliasMatch = existingExactAlias != null; @@ -125,12 +153,6 @@ namespace SharedLibraryCore.Services bool hasExistingAlias = aliases.Count > 0; bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId; - // update each of the aliases where this is no IP but the name is identical - foreach (var alias in aliases.Where(_alias => (_alias.IPAddress == null || _alias.IPAddress == 0))) - { - alias.IPAddress = ip; - } - await context.SaveChangesAsync(); // this happens when the link we found is different than the one we create before adding an IP @@ -191,9 +213,12 @@ namespace SharedLibraryCore.Services await context.SaveChangesAsync(); - entity.CurrentServer.Logger.WriteDebug($"[updatealias] {entity} has exact alias match, so we're going to try to remove aliasId {oldAlias.AliasId} with linkId {oldAlias.AliasId}"); - context.Aliases.Remove(oldAlias); - await context.SaveChangesAsync(); + if (context.Entry(oldAlias).State != EntityState.Deleted) + { + entity.CurrentServer.Logger.WriteDebug($"[updatealias] {entity} has exact alias match, so we're going to try to remove aliasId {oldAlias.AliasId} with linkId {oldAlias.AliasId}"); + context.Aliases.Remove(oldAlias); + await context.SaveChangesAsync(); + } } } @@ -216,6 +241,8 @@ namespace SharedLibraryCore.Services entity.CurrentAliasId = 0; await context.SaveChangesAsync(); } + + entity.CurrentServer.Manager.GetLogger(0).WriteDebug($"End update alias for {entity}"); } /// @@ -286,18 +313,41 @@ namespace SharedLibraryCore.Services throw new NotImplementedException(); } - public async Task Get(int entityID) + public async Task Get(int entityId) { + // todo: this needs to be optimized for large linked accounts using (var context = new DatabaseContext(true)) { - var iqClient = from _c in context.Clients - .Include(c => c.CurrentAlias) - .Include(c => c.AliasLink.Children) - .Include(c => c.Meta) - where _c.ClientId == entityID - select _c; - var client = await iqClient.FirstOrDefaultAsync(); + var client = context.Clients + .Select(_client => new EFClient() + { + ClientId = _client.ClientId, + AliasLinkId = _client.AliasLinkId, + Level = _client.Level, + Connections = _client.Connections, + FirstConnection = _client.FirstConnection, + LastConnection = _client.LastConnection, + Masked = _client.Masked, + NetworkId = _client.NetworkId, + CurrentAlias = new EFAlias() + { + Name = _client.CurrentAlias.Name, + IPAddress = _client.CurrentAlias.IPAddress + } + }) + .FirstOrDefault(_client => _client.ClientId == entityId); + + client.AliasLink = new EFAliasLink() + { + Children = await context.Aliases + .Where(_alias => _alias.LinkId == client.AliasLinkId) + .Select(_alias => new EFAlias() + { + Name = _alias.Name, + IPAddress = _alias.IPAddress + }).ToListAsync() + }; if (client == null) { @@ -337,8 +387,19 @@ namespace SharedLibraryCore.Services EF.CompileAsyncQuery((DatabaseContext context, long networkId) => context.Clients .Include(c => c.CurrentAlias) - .Include(c => c.AliasLink.Children) - .Include(c => c.ReceivedPenalties) + //.Include(c => c.AliasLink.Children) + //.Include(c => c.ReceivedPenalties) + .Select(_client => new EFClient() + { + ClientId = _client.ClientId, + AliasLinkId = _client.AliasLinkId, + Level = _client.Level, + Connections = _client.Connections, + FirstConnection = _client.FirstConnection, + LastConnection = _client.LastConnection, + Masked = _client.Masked, + NetworkId = _client.NetworkId + }) .FirstOrDefault(c => c.NetworkId == networkId) ); @@ -598,7 +659,7 @@ namespace SharedLibraryCore.Services IPAddress = _client.CurrentAlias.IPAddress.ConvertIPtoString(), LastConnection = _client.FirstConnection }); - + #if DEBUG var sql = iqClients.ToSql(); #endif diff --git a/SharedLibraryCore/Services/MetaService.cs b/SharedLibraryCore/Services/MetaService.cs index dd2da0b6..8f68f544 100644 --- a/SharedLibraryCore/Services/MetaService.cs +++ b/SharedLibraryCore/Services/MetaService.cs @@ -74,6 +74,13 @@ namespace SharedLibraryCore.Services return await ctx.EFMeta .Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.ClientId == client.ClientId) + .Select(_meta => new EFMeta() + { + MetaId = _meta.MetaId, + Key = _meta.Key, + ClientId = _meta.ClientId, + Value = _meta.Value + }) .FirstOrDefaultAsync(); } } diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 21ca049d..ffbd2334 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -48,7 +48,7 @@ - + diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index ee5968e5..5d980dab 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -31,7 +31,7 @@ namespace SharedLibraryCore #endif public static Encoding EncodingType; public static Localization.Layout CurrentLocalization = new Localization.Layout(new Dictionary()); - public static TimeSpan DefaultCommandTimeout = new TimeSpan(0, 0, 10); + public static TimeSpan DefaultCommandTimeout = new TimeSpan(0, 0, 25); public static EFClient IW4MAdminClient(Server server = null) { diff --git a/WebfrontCore/Controllers/ClientController.cs b/WebfrontCore/Controllers/ClientController.cs index 52bbb13a..492f4d46 100644 --- a/WebfrontCore/Controllers/ClientController.cs +++ b/WebfrontCore/Controllers/ClientController.cs @@ -45,17 +45,17 @@ namespace WebfrontCore.Controllers NetworkId = client.NetworkId, Meta = new List(), Aliases = client.AliasLink.Children - .Where(a => a.Name != client.Name) .Select(a => a.Name) - .Distinct() + .Prepend(client.Name) .OrderBy(a => a) + .Distinct() .ToList(), IPs = client.AliasLink.Children + .Where(i => i.IPAddress != null) + .OrderByDescending(i => i.DateAdded) .Select(i => i.IPAddress.ConvertIPtoString()) - .Union(new List() { client.CurrentAlias.IPAddress.ConvertIPtoString() }) - .Where(i => !string.IsNullOrEmpty(i)) + .Prepend(client.CurrentAlias.IPAddress.ConvertIPtoString()) .Distinct() - .OrderBy(i => i) .ToList(), HasActivePenalty = activePenalties.Count() > 0, ActivePenaltyType = activePenalties.Count() > 0 ? activePenalties.First().Type.ToString() : null, @@ -138,8 +138,8 @@ namespace WebfrontCore.Controllers } var clientsDto = await Manager.GetClientService().FindClientsByIdentifier(clientName); - - foreach(var client in clientsDto) + + foreach (var client in clientsDto) { if (!Authorized && ((Permission)client.LevelInt).ShouldHideLevel()) { @@ -152,7 +152,7 @@ namespace WebfrontCore.Controllers return View("Find/Index", clientsDto); } - public async Task Meta(int id, int count, int offset, DateTime? startAt) + public async Task GetMeta(int id, int count, int offset, DateTime? startAt) { IEnumerable meta = await MetaService.GetRuntimeMeta(id, startAt == null ? offset : 0, count, startAt ?? DateTime.UtcNow); @@ -160,7 +160,7 @@ namespace WebfrontCore.Controllers { meta = meta.Where(_meta => !_meta.Sensitive); } - + if (meta.Count() == 0) { return Ok(); diff --git a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs index 5a99d355..43525d30 100644 --- a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs +++ b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs @@ -23,7 +23,7 @@ namespace WebfrontCore.Middleware public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager) { _manager = manager; - _manager.OnServerEvent += OnGameEvent; + //_manager.OnServerEvent += OnGameEvent; _privilegedClientIds = new List(); _nextRequest = nextRequest; }