diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index c4ff88576..988b1e1b2 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 71faa2906..26ad6378d 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 6b44d533a..fdfb22c83 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 d886e81f8..695080e52 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 f070d6817..1ed682c99 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 000000000..0b884f670 --- /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 0f8890439..e1962db8e 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 14d6ca653..4ca038eca 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 3ee54e69d..036116975 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 73b44bc39..e1c9af66d 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 8f1e9298a..0943ea460 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 c093b4275..bd0604ada 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 597a3bcbb..92da5a042 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 8f4f4a1c7..980467141 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 69c65932c..9f0a984a8 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 2f763ee39..d51a51011 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 58a8d89fb..66a568753 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 846a00dec..27ad276ca 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 8d48b72f5..dbd0167c2 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 000000000..67e9bc334 --- /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 000000000..a3d604e94 --- /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 066db2b63..6350362c7 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 d779c1aef..587b7eb79 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 aecebd39b..92cd83ef0 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 440291a86..5cea93f79 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 795294dfa..40e336661 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 dd2da0b63..8f68f5444 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 21ca049df..ffbd23342 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -48,7 +48,7 @@ - + diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index ee5968e58..5d980dab8 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 52bbb13a6..492f4d461 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 5a99d3556..43525d30a 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; }