diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 0990ae28..b4485a33 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -1002,10 +1002,13 @@ namespace IW4MAdmin LastMessage = DateTime.Now - start; lastCount = DateTime.Now; + var appConfig = _serviceProvider.GetService(); // update the player history - if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval) + if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval) { - while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours + var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes / + appConfig.ServerDataCollectionInterval.TotalMinutes); + while ( ClientHistory.Count > maxItems) { ClientHistory.Dequeue(); } diff --git a/Application/Migration/DatabaseHousekeeping.cs b/Application/Migration/DatabaseHousekeeping.cs index d4331dd9..d8814675 100644 --- a/Application/Migration/DatabaseHousekeeping.cs +++ b/Application/Migration/DatabaseHousekeeping.cs @@ -4,8 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Data.Abstractions; using Data.Models.Client.Stats; -using SharedLibraryCore.Database; -using SharedLibraryCore.Interfaces; namespace IW4MAdmin.Application.Migration { diff --git a/Application/Misc/ServerDataCollector.cs b/Application/Misc/ServerDataCollector.cs index 6bda0d40..91f8be53 100644 --- a/Application/Misc/ServerDataCollector.cs +++ b/Application/Misc/ServerDataCollector.cs @@ -10,6 +10,7 @@ using Data.Models.Server; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; +using SharedLibraryCore.Configuration; using ILogger = Microsoft.Extensions.Logging.ILogger; using SharedLibraryCore.Interfaces; @@ -21,14 +22,16 @@ namespace IW4MAdmin.Application.Misc private readonly ILogger _logger; private readonly IManager _manager; private readonly IDatabaseContextFactory _contextFactory; + private readonly ApplicationConfiguration _appConfig; private bool _inProgress; private TimeSpan _period; - public ServerDataCollector(ILogger logger, IManager manager, - IDatabaseContextFactory contextFactory) + public ServerDataCollector(ILogger logger, ApplicationConfiguration appConfig, + IManager manager, IDatabaseContextFactory contextFactory) { _logger = logger; + _appConfig = appConfig; _manager = manager; _contextFactory = contextFactory; } @@ -42,7 +45,9 @@ namespace IW4MAdmin.Application.Misc _logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector)); _inProgress = true; - _period = period ?? TimeSpan.FromMinutes(Utilities.IsDevelopment ? 1 : 5); + _period = period ?? (Utilities.IsDevelopment + ? TimeSpan.FromMinutes(1) + : _appConfig.ServerDataCollectionInterval); while (!cancellationToken.IsCancellationRequested) { diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs index e27e617a..0dc84b19 100644 --- a/Application/Misc/ServerDataViewer.cs +++ b/Application/Misc/ServerDataViewer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,8 @@ using Data.Models.Server; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -19,16 +22,19 @@ namespace IW4MAdmin.Application.Misc private readonly ILogger _logger; private readonly IDataValueCache _snapshotCache; private readonly IDataValueCache _serverStatsCache; + private readonly IDataValueCache> _clientHistoryCache; private readonly TimeSpan? _cacheTimeSpan = Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) TimeSpan.FromMinutes(1); public ServerDataViewer(ILogger logger, IDataValueCache snapshotCache, - IDataValueCache serverStatsCache) + IDataValueCache serverStatsCache, + IDataValueCache> clientHistoryCache) { _logger = logger; _snapshotCache = snapshotCache; _serverStatsCache = serverStatsCache; + _clientHistoryCache = clientHistoryCache; } public async Task MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, @@ -45,14 +51,14 @@ namespace IW4MAdmin.Application.Misc { maxClients = await snapshots.Where(snapshot => snapshot.ServerId == serverId) .Where(snapshot => snapshot.CapturedAt >= oldestEntry) - .MaxAsync(snapshot => (int?)snapshot.ClientCount, cancellationToken) ?? 0; + .MaxAsync(snapshot => (int?) snapshot.ClientCount, cancellationToken) ?? 0; } else { maxClients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry) .GroupBy(snapshot => snapshot.PeriodBlock) - .Select(grp => grp.Sum(snapshot => (int?)snapshot.ClientCount)) + .Select(grp => grp.Sum(snapshot => (int?) snapshot.ClientCount)) .MaxAsync(cancellationToken) ?? 0; } @@ -95,5 +101,43 @@ namespace IW4MAdmin.Application.Misc return (0, 0); } } + + public async Task> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default) + { + _clientHistoryCache.SetCacheItem(async (set, cancellationToken) => + { + var oldestEntry = overPeriod.HasValue + ? DateTime.UtcNow - overPeriod.Value + : DateTime.UtcNow.AddHours(-12); + + var history = await set.Where(snapshot => snapshot.CapturedAt >= oldestEntry) + .Select(snapshot => + new + { + snapshot.ServerId, + snapshot.CapturedAt, + snapshot.ClientCount + }) + .OrderBy(snapshot => snapshot.CapturedAt) + .ToListAsync(cancellationToken); + + return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo + { + ServerId = byServer.Key, + ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot() + {Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList() + }).ToList(); + }, nameof(_clientHistoryCache), TimeSpan.MaxValue); + + try + { + return await _clientHistoryCache.GetCacheItem(nameof(_clientHistoryCache), token); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientHistoryAsync)); + return Enumerable.Empty(); + } + } } } \ No newline at end of file diff --git a/Data/Data.csproj b/Data/Data.csproj index 781f7716..850cde95 100644 --- a/Data/Data.csproj +++ b/Data/Data.csproj @@ -8,7 +8,7 @@ RaidMax.IW4MAdmin.Data RaidMax.IW4MAdmin.Data - 1.0.5 + 1.0.6 diff --git a/Data/Helpers/DataValueCache.cs b/Data/Helpers/DataValueCache.cs index 3563de5e..140056a8 100644 --- a/Data/Helpers/DataValueCache.cs +++ b/Data/Helpers/DataValueCache.cs @@ -22,33 +22,36 @@ namespace Data.Helpers public TimeSpan ExpirationTime { get; set; } public Func, CancellationToken, Task> Getter { get; set; } public V Value { get; set; } - public bool IsExpired => (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0; + + public bool IsExpired => ExpirationTime != TimeSpan.MaxValue && + (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0; } - + public DataValueCache(ILogger> logger, IDatabaseContextFactory contextFactory) { _logger = logger; _contextFactory = contextFactory; } - - public void SetCacheItem(Func, CancellationToken, Task> getter, string key, TimeSpan? expirationTime = null) + + public void SetCacheItem(Func, CancellationToken, Task> getter, string key, + TimeSpan? expirationTime = null) { if (_cacheStates.ContainsKey(key)) { _logger.LogDebug("Cache key {key} is already added", key); return; } - + var state = new CacheState() { Key = key, Getter = getter, ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) }; - + _cacheStates.Add(key, state); } - + public async Task GetCacheItem(string keyName, CancellationToken cancellationToken = default) { if (!_cacheStates.ContainsKey(keyName)) @@ -58,7 +61,7 @@ namespace Data.Helpers var state = _cacheStates[keyName]; - if (state.IsExpired) + if (state.IsExpired || state.Value == null) { await RunCacheUpdate(state, cancellationToken); } diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index d9b11b9e..c6339865 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj index 23a4cfaf..0398a3d4 100644 --- a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj +++ b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index 64d2e925..1fe5256b 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -23,7 +23,7 @@ - + diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index ea039921..c28d939c 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -19,7 +19,7 @@ - + diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index 1695465c..c16c6762 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 95b21796..1eecdbff 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -17,7 +17,7 @@ - + diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index de8c395e..a805de73 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -20,7 +20,7 @@ - + diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 0a881ea1..51f85fed 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -141,7 +141,15 @@ namespace SharedLibraryCore.Configuration [LocalizedDisplayName(("WEBFRONT_CONFIGURATION_ENABLE_PRIVILEGED_USER_PRIVACY"))] public bool EnablePrivilegedUserPrivacy { get; set; } + [ConfigurationIgnore] public bool EnableImplicitAccountLinking { get; set; } = false; + + [ConfigurationIgnore] + public TimeSpan MaxClientHistoryTime { get; set; } = TimeSpan.FromHours(12); + + [ConfigurationIgnore] + public TimeSpan ServerDataCollectionInterval { get; set; } = TimeSpan.FromMinutes(5); + public Dictionary OverridePermissionLevelNames { get; set; } = Enum .GetValues(typeof(Permission)) .Cast() diff --git a/SharedLibraryCore/Dtos/ClientHistoryInfo.cs b/SharedLibraryCore/Dtos/ClientHistoryInfo.cs new file mode 100644 index 00000000..7d702834 --- /dev/null +++ b/SharedLibraryCore/Dtos/ClientHistoryInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace SharedLibraryCore.Dtos +{ + public class ClientHistoryInfo + { + public long ServerId { get; set; } + public List ClientCounts { get; set; } + } + + public class ClientCountSnapshot + { + public DateTime Time { get; set; } + public string TimeString => Time.ToString("yyyy-MM-ddTHH:mm:ssZ"); + public int ClientCount { get; set; } + } +} \ No newline at end of file diff --git a/SharedLibraryCore/Dtos/ServerInfo.cs b/SharedLibraryCore/Dtos/ServerInfo.cs index 344c3d10..94fc602d 100644 --- a/SharedLibraryCore/Dtos/ServerInfo.cs +++ b/SharedLibraryCore/Dtos/ServerInfo.cs @@ -17,6 +17,7 @@ namespace SharedLibraryCore.Dtos public List ChatHistory { get; set; } public List Players { get; set; } public Helpers.PlayerHistory[] PlayerHistory { get; set; } + public List ClientCountHistory { get; set; } public long ID { get; set; } public bool Online { get; set; } public string ConnectProtocolUrl { get; set; } diff --git a/SharedLibraryCore/Helpers/PlayerHistory.cs b/SharedLibraryCore/Helpers/PlayerHistory.cs index 2b110451..4039662f 100644 --- a/SharedLibraryCore/Helpers/PlayerHistory.cs +++ b/SharedLibraryCore/Helpers/PlayerHistory.cs @@ -1,4 +1,5 @@ using System; +using SharedLibraryCore.Dtos; namespace SharedLibraryCore.Helpers { @@ -31,5 +32,14 @@ namespace SharedLibraryCore.Helpers /// Used by CanvasJS as a point on the y axis /// public int y { get; } + + public ClientCountSnapshot ToClientCountSnapshot() + { + return new ClientCountSnapshot + { + ClientCount = y, + Time = When + }; + } } } diff --git a/SharedLibraryCore/Interfaces/IServerDataViewer.cs b/SharedLibraryCore/Interfaces/IServerDataViewer.cs index 648cf03c..53df5553 100644 --- a/SharedLibraryCore/Interfaces/IServerDataViewer.cs +++ b/SharedLibraryCore/Interfaces/IServerDataViewer.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Helpers; namespace SharedLibraryCore.Interfaces { @@ -22,8 +25,16 @@ namespace SharedLibraryCore.Interfaces /// Gets the total number of clients connected and total clients connected in the given time frame /// /// how far in the past to search - /// + /// CancellationToken /// Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default); + + /// + /// Retrieves the client count and history over the given period + /// + /// how far in the past to search + /// CancellationToken + /// + Task> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default); } } \ No newline at end of file diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 6dc9d347..b676fa02 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -4,7 +4,7 @@ Library netcoreapp3.1 RaidMax.IW4MAdmin.SharedLibraryCore - 2021.8.27.1 + 2021.8.29.1 RaidMax Forever None Debug;Release;Prerelease @@ -19,7 +19,7 @@ true MIT Shared Library for IW4MAdmin - 2021.8.27.1 + 2021.8.29.1 @@ -44,7 +44,7 @@ - + diff --git a/WebfrontCore/ViewComponents/ServerListViewComponent.cs b/WebfrontCore/ViewComponents/ServerListViewComponent.cs index 6ebd71d5..f1dfc5c5 100644 --- a/WebfrontCore/ViewComponents/ServerListViewComponent.cs +++ b/WebfrontCore/ViewComponents/ServerListViewComponent.cs @@ -1,45 +1,94 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; using SharedLibraryCore; using SharedLibraryCore.Dtos; using System.Linq; using System.Net; +using System.Threading; +using System.Threading.Tasks; using Data.Models.Client.Stats; +using Microsoft.AspNetCore.Hosting.Server; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Interfaces; using static SharedLibraryCore.Server; namespace WebfrontCore.ViewComponents { public class ServerListViewComponent : ViewComponent { + private readonly IServerDataViewer _serverDataViewer; + private readonly ApplicationConfiguration _appConfig; + + public ServerListViewComponent(IServerDataViewer serverDataViewer, + ApplicationConfiguration applicationConfiguration) + { + _serverDataViewer = serverDataViewer; + _appConfig = applicationConfiguration; + } + public IViewComponentResult Invoke(Game? game) { - var servers = Program.Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game); + var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game); - var serverInfo = servers.Select(s => new ServerInfo() + var serverInfo = new List(); + + foreach (var server in servers) { - Name = s.Hostname, - ID = s.EndPoint, - Port = s.Port, - Map = s.CurrentMap.Alias, - ClientCount = s.ClientNum, - MaxClients = s.MaxClients, - GameType = s.Gametype, - PlayerHistory = s.ClientHistory.ToArray(), - Players = s.GetClientsAsList() - .Select(p => new PlayerInfo() + var serverId = server.GetIdForServer().Result; + var clientHistory = _serverDataViewer.ClientHistoryAsync(_appConfig.MaxClientHistoryTime, + CancellationToken.None).Result + .FirstOrDefault(history => history.ServerId == serverId) ?? + new ClientHistoryInfo + { + ServerId = serverId + }; + + var counts = clientHistory.ClientCounts?.AsEnumerable() ?? Enumerable.Empty(); + + if (server.ClientHistory.Count > 0) { - Name = p.Name, - ClientId = p.ClientId, - Level = p.Level.ToLocalizedLevelName(), - LevelInt = (int)p.Level, - Tag = p.Tag, - ZScore = p.GetAdditionalProperty(IW4MAdmin.Plugins.Stats.Helpers.StatManager.CLIENT_STATS_KEY)?.ZScore - }).ToList(), - ChatHistory = s.ChatHistory.ToList(), - Online = !s.Throttled, - IPAddress = $"{(s.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : s.IP)}:{s.Port}", - ConnectProtocolUrl = s.EventParser.URLProtocolFormat.FormatExt(s.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : s.IP, s.Port) - }).ToList(); + counts = counts.Union(server.ClientHistory + .Select(history => history.ToClientCountSnapshot()).Where(history => + history.Time > clientHistory.ClientCounts.Last().Time)); + } + + serverInfo.Add(new ServerInfo() + { + Name = server.Hostname, + ID = server.EndPoint, + Port = server.Port, + Map = server.CurrentMap.Alias, + ClientCount = server.ClientNum, + MaxClients = server.MaxClients, + GameType = server.Gametype, + PlayerHistory = server.ClientHistory.ToArray(), + Players = server.GetClientsAsList() + .Select(p => new PlayerInfo() + { + Name = p.Name, + ClientId = p.ClientId, + Level = p.Level.ToLocalizedLevelName(), + LevelInt = (int) p.Level, + Tag = p.Tag, + ZScore = p.GetAdditionalProperty(IW4MAdmin.Plugins.Stats.Helpers + .StatManager + .CLIENT_STATS_KEY)?.ZScore + }).ToList(), + ChatHistory = server.ChatHistory.ToList(), + ClientCountHistory = + counts.Where(history => history.Time >= DateTime.UtcNow - _appConfig.MaxClientHistoryTime) + .ToList(), + Online = !server.Throttled, + IPAddress = + $"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}", + ConnectProtocolUrl = server.EventParser.URLProtocolFormat.FormatExt( + server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP, + server.Port) + }); + } + return View("_List", serverInfo); } } -} +} \ No newline at end of file diff --git a/WebfrontCore/Views/Server/_Server.cshtml b/WebfrontCore/Views/Server/_Server.cshtml index b0cbd94c..28534538 100644 --- a/WebfrontCore/Views/Server/_Server.cshtml +++ b/WebfrontCore/Views/Server/_Server.cshtml @@ -50,5 +50,8 @@
-
+
\ No newline at end of file diff --git a/WebfrontCore/wwwroot/js/server.js b/WebfrontCore/wwwroot/js/server.js index 89e3dc52..ba491516 100644 --- a/WebfrontCore/wwwroot/js/server.js +++ b/WebfrontCore/wwwroot/js/server.js @@ -2,7 +2,8 @@ /////////////////////////////////////// // thanks to canvasjs :( playerHistory.forEach(function (item, i) { - playerHistory[i].x = new Date(playerHistory[i].x); + playerHistory[i].x = new Date(playerHistory[i].timeString); + playerHistory[i].y = playerHistory[i].clientCount; }); return new CanvasJS.Chart(`server_history_${i}`, { @@ -84,7 +85,7 @@ $(document).ready(function () { }); $('.server-history-row').each(function (index, element) { - let clientHistory = $(this).data('clienthistory'); + let clientHistory = $(this).data('clienthistory-ex'); let serverId = $(this).data('serverid'); let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text()); let primaryColor = $('title').css('background-color');