Compare commits

..

7 Commits

Author SHA1 Message Date
e56f574af4 fix introduced bug :) 2020-10-01 19:06:12 -05:00
513f495304 anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-10-01 19:05:52 -05:00
910faf427b enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-10-01 19:05:38 -05:00
9117440566 merge to pre (#172)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x

* implement pm admins command for issue #170

* implement service resolver for script plugins
2020-09-26 18:15:56 -05:00
ad6b6a6465 merge to pre (#169)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x
2020-09-21 15:37:31 -05:00
eb8145a168 add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-04 12:58:54 -05:00
a560e05df8 update pipeline file for seperate builds 2020-08-31 12:33:39 -05:00
980 changed files with 13621 additions and 201056 deletions

5
.gitignore vendored
View File

@ -224,6 +224,7 @@ bootstrap-custom.min.css
bootstrap-custom.css bootstrap-custom.css
**/Master/static **/Master/static
**/Master/dev_env **/Master/dev_env
/WebfrontCore/Views/Plugins/*
/WebfrontCore/wwwroot/**/dds /WebfrontCore/wwwroot/**/dds
/WebfrontCore/wwwroot/images/radar/* /WebfrontCore/wwwroot/images/radar/*
@ -243,7 +244,3 @@ launchSettings.json
/Tests/ApplicationTests/Files/GameEvents.json /Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json /Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env /GameLogServer/game_log_server_env
.idea/*
*.db
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Application.API.Master
public int Uptime { get; set; } public int Uptime { get; set; }
/// <summary> /// <summary>
/// Specifies the version of the instance /// Specifices the version of the instance
/// </summary> /// </summary>
[JsonProperty("version")] [JsonProperty("version")]
[JsonConverter(typeof(BuildNumberJsonConverter))] [JsonConverter(typeof(BuildNumberJsonConverter))]
@ -33,11 +33,5 @@ namespace IW4MAdmin.Application.API.Master
/// </summary> /// </summary>
[JsonProperty("servers")] [JsonProperty("servers")]
public List<ApiServer> Servers { get; set; } public List<ApiServer> Servers { get; set; }
/// <summary>
/// Url IW4MAdmin is listening on
/// </summary>
[JsonProperty("webfront_url")]
public string WebfrontUrl { get; set; }
} }
} }

View File

@ -1,7 +1,5 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestEase; using RestEase;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
@ -37,13 +35,6 @@ namespace IW4MAdmin.Application.API.Master
public string Message { get; set; } public string Message { get; set; }
} }
public class PluginSubscriptionContent
{
public string Content { get; set; }
public PluginType Type { get; set; }
}
/// <summary> /// <summary>
/// Defines the capabilities of the master API /// Defines the capabilities of the master API
/// </summary> /// </summary>
@ -72,8 +63,5 @@ namespace IW4MAdmin.Application.API.Master
[Get("localization/{languageTag}")] [Get("localization/{languageTag}")]
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag); Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
[Get("plugin_subscriptions")]
Task<IEnumerable<PluginSubscriptionContent>> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id);
} }
} }

View File

@ -1,55 +0,0 @@
using System;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.Alerts;
public static class AlertExtensions
{
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
{
return new Alert.AlertState
{
RecipientId = client.ClientId,
Category = type ?? Alert.AlertCategory.Information
};
}
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
{
state.Category = category;
return state;
}
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
{
state.Type = type;
return state;
}
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
{
state.Message = message;
return state;
}
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
{
state.ExpiresAt = DateTime.Now.Add(expiration);
return state;
}
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
{
state.Source = source;
return state;
}
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
{
state.Source = client.Name.StripColors();
state.SourceId = client.ClientId;
return state;
}
}

View File

@ -1,137 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Alerts;
public class AlertManager : IAlertManager
{
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
public AlertManager(ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_states.TryAdd(0, new List<Alert.AlertState>());
}
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
public async Task Initialize()
{
foreach (var source in _staticSources)
{
var alerts = await source();
foreach (var alert in alerts)
{
AddAlert(alert);
}
}
}
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
lock (_states)
{
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
alerts = alerts.Concat(_states[0].Where(alert =>
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
}
if (_states.ContainsKey(client.ClientId))
{
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
if (matchingEvent is null)
{
continue;
}
items.Remove(matchingEvent);
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
}
public void AddAlert(Alert.AlertState alert)
{
lock (_states)
{
if (alert.RecipientId is null)
{
_states[0].Add(alert);
return;
}
if (!_states.ContainsKey(alert.RecipientId.Value))
{
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
}
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
{
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
}
_states[alert.RecipientId.Value].Add(alert);
PruneOldAlerts();
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
{
_staticSources.Add(alertSource);
}
private void PruneOldAlerts()
{
foreach (var value in _states.Values)
{
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
}
}
}

View File

@ -2,10 +2,10 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2020.0.0.0</Version> <Version>2.3.2.0</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Product>IW4MAdmin</Product> <Product>IW4MAdmin</Product>
@ -24,15 +24,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2038" /> <PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
<PackageReference Include="RestEase" Version="1.5.5" /> <PackageReference Include="RestEase" Version="1.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -40,6 +39,7 @@
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
<StartupObject></StartupObject>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -49,8 +49,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Integrations\Cod\Integrations.Cod.csproj" />
<ProjectReference Include="..\Integrations\Source\Integrations.Source.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj"> <ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private> <Private>true</Private>
</ProjectReference> </ProjectReference>
@ -61,12 +59,6 @@
<None Update="DefaultSettings.json"> <None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\GeoLite2-Country.mmdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@ -1,36 +1,30 @@
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RConParsers; using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
using IW4MAdmin.Application.Migration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Formatting;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
@ -38,7 +32,7 @@ namespace IW4MAdmin.Application
{ {
private readonly ConcurrentBag<Server> _servers; private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
[Obsolete] public ObsoleteLogger Logger => _serviceProvider.GetRequiredService<ObsoleteLogger>(); public ILogger Logger => GetLogger(0);
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; } public DateTime StartTime { get; private set; }
@ -56,10 +50,12 @@ namespace IW4MAdmin.Application
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private readonly CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>(); private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
@ -72,33 +68,30 @@ namespace IW4MAdmin.Application
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration; private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver; private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = new ClientService(contextFactory);
PenaltySvc = penaltyService; AliasSvc = new AliasService();
_alertManager = alertManager; PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) }; AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_metaService = metaService;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList(); _commands = commands.ToList();
_translationLookup = translationLookup; _translationLookup = translationLookup;
_commandConfiguration = commandConfiguration; _commandConfiguration = commandConfiguration;
@ -109,9 +102,6 @@ namespace IW4MAdmin.Application
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver; _scriptPluginServiceResolver = scriptPluginServiceResolver;
_serviceProvider = serviceProvider;
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins; Plugins = plugins;
} }
@ -119,8 +109,10 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
ProcessingEvents.TryAdd(newEvent.Id, newEvent); #if DEBUG == true
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
#endif
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
{ {
@ -132,17 +124,22 @@ namespace IW4MAdmin.Application
await newEvent.Owner.ExecuteEvent(newEvent); await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database // save the event info to the database
await _changeHistoryService.Add(newEvent); var changeHistorySvc = new ChangeHistoryService();
await changeHistorySvc.Add(newEvent);
#if DEBUG
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
#endif
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id); Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id); Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
} }
// this happens if a plugin requires login // this happens if a plugin requires login
@ -155,58 +152,31 @@ namespace IW4MAdmin.Application
catch (NetworkException ex) catch (NetworkException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString())) Logger.WriteError(ex.Message);
{ Logger.WriteDebug(ex.GetExceptionInfo());
_logger.LogError(ex, ex.Message);
}
} }
catch (ServerException ex) catch (ServerException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString())) Logger.WriteWarning(ex.Message);
{
_logger.LogError(ex, ex.Message);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner)); Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString())) Logger.WriteDebug(ex.GetExceptionInfo());
{
_logger.LogError(ex, "Unexpected exception");
}
}
skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.Id, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
{
ProcessingEvents.Remove(newEvent.Id, out _);
} }
skip:
// tell anyone waiting for the output that we're done // tell anyone waiting for the output that we're done
newEvent.Complete(); newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent); OnGameEventExecuted?.Invoke(this, newEvent);
#if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif
} }
public IList<Server> GetServers() public IList<Server> GetServers()
@ -219,31 +189,31 @@ namespace IW4MAdmin.Application
return _commands; return _commands;
} }
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
public async Task UpdateServerStates() public async Task UpdateServerStates()
{ {
// store the server hash code and task for it // store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>(); var runningUpdateTasks = new Dictionary<long, Task>();
while (!_tokenSource.IsCancellationRequested) while (!_tokenSource.IsCancellationRequested)
{ {
// select the server ids that have completed the update task // select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion || .Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes ut.Value.Status == TaskStatus.Canceled ||
ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5)) ut.Value.Status == TaskStatus.Faulted)
.Select(ut => ut.Key) .Select(ut => ut.Key)
.ToList(); .ToList();
// remove the update tasks as they have completed // this is to prevent the log reader from starting before the initial
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId))) // query of players on the server
if (serverTasksToRemove.Count > 0)
{ {
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested) IsInitialized = true;
{ }
runningUpdateTasks[serverId].tokenSource.Cancel();
}
// remove the update tasks as they have completd
foreach (long serverId in serverTasksToRemove)
{
runningUpdateTasks.Remove(serverId); runningUpdateTasks.Remove(serverId);
} }
@ -251,33 +221,36 @@ namespace IW4MAdmin.Application
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint))) foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{ {
var tokenSource = new CancellationTokenSource(); runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{ {
try try
{ {
if (runningUpdateTasks.ContainsKey(server.EndPoint)) await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{ {
await server.ProcessUpdatesAsync(_tokenSource.Token) await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
} }
} }
catch (Exception e) catch (Exception e)
{ {
using (LogContext.PushProperty("Server", server.ToString())) Logger.WriteWarning($"Failed to update status for {server}");
{ Logger.WriteDebug(e.GetExceptionInfo());
_logger.LogError(e, "Failed to update status");
}
} }
finally finally
{ {
server.IsInitialized = true; server.IsInitialized = true;
} }
}, tokenSource.Token), tokenSource, DateTime.Now)); }));
} }
#if DEBUG
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
try try
{ {
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token); await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
@ -299,15 +272,6 @@ namespace IW4MAdmin.Application
IsRunning = true; IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP(); ExternalIPAddress = await Utilities.GetExternalIP();
#region DATABASE
_logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region PLUGINS #region PLUGINS
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
@ -325,8 +289,8 @@ namespace IW4MAdmin.Application
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name)); Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
_logger.LogError(ex, "Could not properly load plugin {plugin}", scriptPlugin.Name); Logger.WriteDebug(ex.Message);
} }
}; };
} }
@ -339,27 +303,32 @@ namespace IW4MAdmin.Application
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, $"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}"); Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
Logger.WriteDebug(ex.GetExceptionInfo());
} }
} }
#endregion #endregion
#region CONFIG #region CONFIG
// copy over default config if it doesn't exist var config = ConfigHandler.Configuration();
if (!_appConfig.Servers?.Any() ?? true)
{
var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultHandler.BuildAsync();
var defaultConfig = defaultHandler.Configuration();
_appConfig.AutoMessages = defaultConfig.AutoMessages;
_appConfig.GlobalRules = defaultConfig.GlobalRules;
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
//if (newConfig.Servers == null) // copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
newConfig.QuickMessages = defaultConfig.QuickMessages;
if (newConfig.Servers == null)
{ {
ConfigHandler.Set(_appConfig); ConfigHandler.Set(newConfig);
_appConfig.Servers = new ServerConfiguration[1]; newConfig.Servers = new ServerConfiguration[1];
do do
{ {
@ -374,54 +343,43 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser); serverConfig.AddEventParser(parser);
} }
_appConfig.Servers = _appConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray(); newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"])); } while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
} }
else else
{ {
if (string.IsNullOrEmpty(_appConfig.Id)) if (string.IsNullOrEmpty(config.Id))
{ {
_appConfig.Id = Guid.NewGuid().ToString(); config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl)) if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{ {
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624"; config.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
#pragma warning disable 618
if (_appConfig.Maps != null)
{
_appConfig.Maps = null;
}
if (_appConfig.QuickMessages != null)
{
_appConfig.QuickMessages = null;
}
#pragma warning restore 618
var validator = new ApplicationConfigurationValidator(); var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(_appConfig); var validationResult = validator.Validate(config);
if (!validationResult.IsValid) if (!validationResult.IsValid)
{ {
throw new ConfigurationException("Could not validate configuration") throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
{ {
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(), Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(),
ConfigurationFileName = ConfigHandler.FileName ConfigurationFileName = ConfigHandler.FileName
}; };
} }
foreach (var serverConfig in _appConfig.Servers) foreach (var serverConfig in config.Servers)
{ {
ConfigurationMigration.ModifyLogPath020919(serverConfig); Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null) if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{ {
@ -441,29 +399,26 @@ namespace IW4MAdmin.Application
} }
} }
if (_appConfig.Servers.Length == 0) if (config.Servers.Length == 0)
{ {
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
} }
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252"); Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
foreach (var parser in AdditionalRConParsers)
{
if (!parser.Configuration.ColorCodeMapping.ContainsKey(ColorCodes.Accent.ToString()))
{
parser.Configuration.ColorCodeMapping.Add(ColorCodes.Accent.ToString(),
parser.Configuration.ColorCodeMapping.TryGetValue(_appConfig.IngameAccentColorKey, out var colorCode)
? colorCode
: "");
}
}
#endregion #endregion
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
GetApplicationSettings().Configuration()?.DatabaseProvider))
{
await new ContextSeed(db).Seed();
}
#endregion
#region COMMANDS #region COMMANDS
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token)) if (ClientSvc.GetOwners().Result.Count > 0)
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -485,17 +440,13 @@ namespace IW4MAdmin.Application
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily // this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it // inject it to all the places that need it
cmdConfig.CommandPrefix = _appConfig?.CommandPrefix ?? "!"; cmdConfig.CommandPrefix = config.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = _appConfig?.BroadcastCommandPrefix ?? "@"; cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix;
foreach (var cmd in commandsToAddToConfig) foreach (var cmd in commandsToAddToConfig)
{ {
if (cmdConfig.Commands.ContainsKey(cmd.CommandConfigNameForType()))
{
continue;
}
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(), cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties new CommandProperties()
{ {
Name = cmd.Name, Name = cmd.Name,
Alias = cmd.Alias, Alias = cmd.Alias,
@ -510,7 +461,6 @@ namespace IW4MAdmin.Application
#endregion #endregion
_metaRegistration.Register(); _metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -522,9 +472,7 @@ namespace IW4MAdmin.Application
} }
#endregion #endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
IsInitialized = true;
} }
private async Task InitializeServers() private async Task InitializeServers()
@ -539,20 +487,16 @@ namespace IW4MAdmin.Application
{ {
// todo: this might not always be an IW4MServer // todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer; var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
using (LogContext.PushProperty("Server", ServerInstance.ToString())) await ServerInstance.Initialize();
{
_logger.LogInformation("Beginning server communication initialization");
await ServerInstance.Initialize();
_servers.Add(ServerInstance); _servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname);
}
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server // add the start event for this server
var e = new GameEvent() var e = new GameEvent()
{ {
Type = EventType.Start, Type = GameEvent.EventType.Start,
Data = $"{ServerInstance.GameName} started", Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance Owner = ServerInstance
}; };
@ -563,11 +507,13 @@ namespace IW4MAdmin.Application
catch (ServerException e) catch (ServerException e)
{ {
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]")); Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
using (LogContext.PushProperty("Server", $"{Conf.IPAddress}:{Conf.Port}"))
if (e.GetType() == typeof(DvarException))
{ {
_logger.LogError(e, "Unexpected exception occurred during initialization"); Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
} }
lastException = e; lastException = e;
} }
} }
@ -590,35 +536,32 @@ namespace IW4MAdmin.Application
public async Task Start() => await UpdateServerStates(); public async Task Start() => await UpdateServerStates();
public async Task Stop() public void Stop()
{ {
foreach (var plugin in Plugins)
{
try
{
await plugin.OnUnloadAsync().WithTimeout(Utilities.DefaultCommandTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
}
}
_tokenSource.Cancel(); _tokenSource.Cancel();
IsRunning = false; IsRunning = false;
} }
public void Restart() public void Restart()
{ {
IsRestartRequested = true; IsRestartRequested = true;
Stop().GetAwaiter().GetResult(); Stop();
} }
[Obsolete] public ILogger GetLogger(long serverId)
public ObsoleteLogger GetLogger(long serverId)
{ {
return _serviceProvider.GetRequiredService<ObsoleteLogger>(); if (_loggers.ContainsKey(serverId))
{
return _loggers[serverId];
}
else
{
var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
_loggers.Add(serverId, newLogger);
return newLogger;
}
} }
public IList<MessageToken> GetMessageTokens() public IList<MessageToken> GetMessageTokens()
@ -632,16 +575,16 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList(); return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
} }
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client;
public ClientService GetClientService() public ClientService GetClientService()
{ {
return ClientSvc; return ClientSvc;
} }
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService() public PenaltyService GetPenaltyService()
{ {
return PenaltySvc; return PenaltySvc;
@ -664,7 +607,7 @@ namespace IW4MAdmin.Application
public IRConParser GenerateDynamicRConParser(string name) public IRConParser GenerateDynamicRConParser(string name)
{ {
return new DynamicRConParser(_serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), _parserRegexFactory) return new DynamicRConParser(_parserRegexFactory)
{ {
Name = name Name = name
}; };
@ -700,6 +643,5 @@ namespace IW4MAdmin.Application
} }
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
} }
} }

View File

@ -4,9 +4,11 @@ set TargetDir=%3
set OutDir=%4 set OutDir=%4
set Version=%5 set Version=%5
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
echo Copying dependency configs echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%" copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
copy "%SolutionDir%SharedLibraryCore\%OutDir%*.deps.json" "%TargetDir%" copy "%SolutionDir%SharedLibaryCore\%OutDir%*.deps.json" "%TargetDir%"
if not exist "%TargetDir%Plugins" ( if not exist "%TargetDir%Plugins" (
echo "Making plugin dir" echo "Making plugin dir"
@ -14,4 +16,12 @@ if not exist "%TargetDir%Plugins" (
) )
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\" xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
del "%TargetDir%Plugins\SQLite*"
echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"

View File

@ -23,7 +23,6 @@ echo setting up default folders
if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration" if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\" move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\" if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
del "%PublishDir%\Microsoft.CodeAnalysis*.dll" /F /Q
move "%PublishDir%\*.dll" "%PublishDir%\Lib\" move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\" move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes" move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
@ -31,37 +30,16 @@ move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de" move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt" move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es" move "%PublishDir%\es" "%PublishDir%\Lib\es"
rmdir /Q /S "%PublishDir%\cs"
rmdir /Q /S "%PublishDir%\fr"
rmdir /Q /S "%PublishDir%\it"
rmdir /Q /S "%PublishDir%\ja"
rmdir /Q /S "%PublishDir%\ko"
rmdir /Q /S "%PublishDir%\pl"
rmdir /Q /S "%PublishDir%\pt-BR"
rmdir /Q /S "%PublishDir%\tr"
rmdir /Q /S "%PublishDir%\zh-Hans"
rmdir /Q /S "%PublishDir%\zh-Hant"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs" if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd" @(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh" @(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
echo copying update scripts
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.ps1" "%PublishDir%\UpdateIW4MAdmin.ps1"
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.sh" "%PublishDir%\UpdateIW4MAdmin.sh"
echo moving front-end library dependencies echo moving front-end library dependencies
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font" if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\" move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib" if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib"
if not exist "%PublishDir%\wwwroot\css" mkdir "%PublishDir%\wwwroot\css"
move "WebfrontCore\wwwroot\css\global.min.css" "%PublishDir%\wwwroot\css\global.min.css"
if not exist "%PublishDir%\wwwroot\js" mkdir "%PublishDir%\wwwroot\js"
move "%SourceDir%\WebfrontCore\wwwroot\js\global.min.js" "%PublishDir%\wwwroot\js\global.min.js"
if not exist "%PublishDir%\wwwroot\images" mkdir "%PublishDir%\wwwroot\images"
xcopy "%SourceDir%\WebfrontCore\wwwroot\images" "%PublishDir%\wwwroot\images" /E /H /C /I
echo setting permissions... echo setting permissions...
cacls "%PublishDir%" /t /e /p Everyone:F cacls "%PublishDir%" /t /e /p Everyone:F

View File

@ -1,64 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class AddClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public AddClientTagCommand(ILogger<AddClientTagCommand> commandLogger, CommandConfiguration config,
ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "addclienttag";
Description = layout["COMMANDS_ADD_CLIENT_TAG_DESC"];
Alias = "act";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
logger = commandLogger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var existingTags = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2) ??
new List<TagMeta>();
var tagName = gameEvent.Data.Trim();
if (existingTags.Any(tag => tag.TagName == tagName))
{
logger.LogWarning("Tag with name {TagName} already exists", tagName);
return;
}
existingTags.Add(new TagMeta
{
Id = (existingTags.LastOrDefault()?.TagId ?? 0) + 1,
Value = tagName
});
await _metaService.SetPersistentMetaValue(EFMeta.ClientTagNameV2, existingTags,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_TAG_SUCCESS"].FormatExt(gameEvent.Data));
}
}
}

View File

@ -1,38 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class ListClientTags : Command
{
private readonly IMetaServiceV2 _metaService;
public ListClientTags(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(
config, layout)
{
Name = "listclienttags";
Description = layout["COMMANDS_LIST_CLIENT_TAGS_DESC"];
Alias = "lct";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var tags = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2);
if (tags is not null)
{
await gameEvent.Origin.TellAsync(tags.Select(tag => tag.TagName),
gameEvent.Owner.Manager.CancellationToken);
}
}
}
}

View File

@ -1,48 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class RemoveClientTag : Command
{
private readonly IMetaServiceV2 _metaService;
public RemoveClientTag(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(
config, layout)
{
Name = "removeclienttag";
Description = layout["COMMANDS_REMOVE_CLIENT_TAG_DESC"];
Alias = "rct";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var existingMeta = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2,
gameEvent.Owner.Manager.CancellationToken);
existingMeta = existingMeta.Where(meta => meta.TagName != gameEvent.Data.Trim()).ToList();
await _metaService.SetPersistentMetaValue(EFMeta.ClientTagNameV2, existingMeta,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REMOVE_CLIENT_TAG_SUCCESS"].FormatExt(gameEvent.Data));
}
}
}

View File

@ -1,58 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class SetClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public SetClientTagCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "setclienttag";
Description = layout["COMMANDS_SET_CLIENT_TAG_DESC"];
Alias = "sct";
Permission = EFClient.Permission.Owner;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var token = gameEvent.Owner.Manager.CancellationToken;
var availableTags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2, token);
var matchingTag = availableTags.FirstOrDefault(tag => tag.Value == gameEvent.Data.Trim());
if (matchingTag == null)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SET_CLIENT_TAG_FAIL"].FormatExt(gameEvent.Data));
return;
}
gameEvent.Target.Tag = matchingTag.Value;
await _metaService.SetPersistentMetaForLookupKey(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, matchingTag.Id,
gameEvent.Target.ClientId, token);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SET_CLIENT_TAG_SUCCESS"].FormatExt(matchingTag.Value));
}
}
}

View File

@ -1,13 +0,0 @@
using System.Text.Json.Serialization;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags;
public class TagMeta : ILookupValue<string>
{
[JsonIgnore] public int TagId => Id;
[JsonIgnore] public string TagName => Value;
public int Id { get; set; }
public string Value { get; set; }
}

View File

@ -1,43 +0,0 @@
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class UnsetClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public UnsetClientTagCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "unsetclienttag";
Description = layout["COMMANDS_UNSET_CLIENT_TAG_DESC"];
Alias = "uct";
Permission = EFClient.Permission.Owner;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Target.Tag = null;
await _metaService.RemovePersistentMeta(EFMeta.ClientTagV2, gameEvent.Target.ClientId,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_UNSET_CLIENT_TAG_SUCCESS"]);
}
}
}

View File

@ -1,59 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Finds player by name
/// </summary>
public class FindPlayerCommand : Command
{
public FindPlayerCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "find";
Description = _translationLookup["COMMANDS_FIND_DESC"];
Alias = "f";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data.Length < 3)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_MIN"]);
return;
}
var players = await gameEvent.Owner.Manager.GetClientService().FindClientsByIdentifier(gameEvent.Data);
if (!players.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_EMPTY"]);
return;
}
foreach (var client in players)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT_V2"].FormatExt(client.Name,
client.ClientId, Utilities.ConvertLevelToColor((EFClient.Permission) client.LevelInt, client.Level),
client.IPAddress, (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture()));
}
}
}
}

View File

@ -1,93 +0,0 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints help information
/// </summary>
public class HelpCommand : Command
{
public HelpCommand(CommandConfiguration config, ITranslationLookup translationLookup) :
base(config, translationLookup)
{
Name = "help";
Description = translationLookup["COMMANDS_HELP_DESC"];
Alias = "h";
Permission = EFClient.Permission.User;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_COMMANDS"],
Required = false
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var searchTerm = gameEvent.Data.Trim();
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
command.SupportedGames == null || !command.SupportedGames.Any() ||
command.SupportedGames.Contains(gameEvent.Owner.GameName))
.Where(command => gameEvent.Origin.Level >= command.Permission);
if (searchTerm.Length > 2)
{
var matchingCommand = availableCommands.FirstOrDefault(command =>
command.Name.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
command.Alias.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase));
if (matchingCommand != null)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_SEARCH_RESULT"]
.FormatExt(matchingCommand.Name, matchingCommand.Alias));
gameEvent.Origin.Tell(matchingCommand.Syntax);
}
else
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_NOTFOUND"]);
}
}
else
{
var commandStrings = availableCommands.Select((command, index) =>
new
{
response = $" {_translationLookup["COMMANDS_HELP_LIST_FORMAT"].FormatExt(command.Name)} ",
index
});
var helpResponse = new StringBuilder();
foreach (var item in commandStrings)
{
helpResponse.Append(item.response);
if (item.index == 0 || item.index % 4 != 0)
{
continue;
}
gameEvent.Origin.Tell(helpResponse.ToString());
helpResponse = new StringBuilder();
}
gameEvent.Origin.Tell(helpResponse.ToString());
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_MOREINFO"]);
}
return Task.CompletedTask;
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists all unmasked admins
/// </summary>
public class ListAdminsCommand : Command
{
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "admins";
Description = _translationLookup["COMMANDS_ADMINS_DESC"];
Alias = "a";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public static string OnlineAdmins(Server server, ITranslationLookup lookup)
{
var onlineAdmins = server.GetClientsAsList()
.Where(p => p.Level > EFClient.Permission.Flagged)
.Where(p => !p.Masked)
.Select(p =>
$"[(Color::Yellow){Utilities.ConvertLevelToColor(p.Level, p.ClientPermission.Name)}(Color::White)] {p.Name}")
.ToList();
return onlineAdmins.Any() ? string.Join(Environment.NewLine, onlineAdmins) : lookup["COMMANDS_ADMINS_NONE"];
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
foreach (var line in OnlineAdmins(gameEvent.Owner, _translationLookup).Split(Environment.NewLine))
{
var _ = gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)
? gameEvent.Owner.Broadcast(line)
: gameEvent.Origin.Tell(line);
}
return Task.CompletedTask;
}
}
}

View File

@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists alises of specified client
/// </summary>
public class ListAliasesCommand : Command
{
public ListAliasesCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "alias";
Description = _translationLookup["COMMANDS_ALIAS_DESC"];
Alias = "known";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true,
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = new StringBuilder();
var names = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.Name));
var ips = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.IPAddress.ConvertIPtoString())
.Distinct());
gameEvent.Origin.Tell($"[(Color::Accent){gameEvent.Target}(Color::White)]");
message.Append($"{_translationLookup["COMMANDS_ALIAS_ALIASES"]}: ");
message.Append(string.Join(" | ", names));
gameEvent.Origin.Tell(message.ToString());
message.Clear();
message.Append($"{_translationLookup["COMMANDS_ALIAS_IPS"]}: ");
message.Append(string.Join(" | ", ips));
gameEvent.Origin.Tell(message.ToString());
return Task.CompletedTask;
}
}
}

View File

@ -1,37 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List online clients
/// </summary>
public class ListClientsCommand : Command
{
public ListClientsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "list";
Description = _translationLookup["COMMANDS_LIST_DESC"];
Alias = "l";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var clientList = gameEvent.Owner.GetClientsAsList()
.Select(client =>
$"[(Color::Accent){client.ClientPermission.Name}(Color::White){(string.IsNullOrEmpty(client.Tag) ? "" : $" {client.Tag}")}(Color::White)][(Color::Yellow)#{client.ClientNumber}(Color::White)] {client.Name}")
.ToArray();
gameEvent.Origin.TellAsync(clientList, gameEvent.Owner.Manager.CancellationToken);
return Task.CompletedTask;
}
}
}

View File

@ -1,41 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists the loaded plugins
/// </summary>
public class ListPluginsCommand : Command
{
private readonly IEnumerable<IPlugin> _plugins;
public ListPluginsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IEnumerable<IPlugin> plugins) : base(config, translationLookup)
{
Name = "plugins";
Description = _translationLookup["COMMANDS_PLUGINS_DESC"];
Alias = "p";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
_plugins = plugins;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PLUGINS_LOADED"]);
foreach (var plugin in _plugins.Where(plugin => !plugin.IsParser))
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_LIST_PLUGINS_FORMAT"]
.FormatExt(plugin.Name, plugin.Version, plugin.Author));
}
return Task.CompletedTask;
}
}
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List all reports on the server
/// </summary>
public class ListReportsCommand : Command
{
public ListReportsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "reports";
Description = _translationLookup["COMMANDS_REPORTS_DESC"];
Alias = "reps";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_CLEAR"],
Required = false
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data != null && gameEvent.Data.ToLower().Contains(_translationLookup["COMMANDS_ARGS_CLEAR"]))
{
gameEvent.Owner.Reports = new List<Report>();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_CLEAR_SUCCESS"]);
return Task.CompletedTask;
}
if (gameEvent.Owner.Reports.Count < 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_NONE"]);
return Task.CompletedTask;
}
foreach (var report in gameEvent.Owner.Reports)
{
gameEvent.Origin.Tell(
$"(Color::Accent){report.Origin.Name}(Color::White) -> (Color::Red){report.Target.Name}(Color::White): {report.Reason}");
}
return Task.CompletedTask;
}
}
}

View File

@ -1,116 +0,0 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Extensions;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class MapAndGameTypeCommand : Command
{
private const string ArgumentRegexPattern = "(?:\"([^\"]+)\"|([^\\s]+)) (?:\"([^\"]+)\"|([^\\s]+))";
private readonly ILogger _logger;
private readonly DefaultSettings _defaultSettings;
public MapAndGameTypeCommand(ILogger<MapAndGameTypeCommand> logger, CommandConfiguration config,
DefaultSettings defaultSettings, ITranslationLookup layout) : base(config, layout)
{
Name = "mapandgametype";
Description = _translationLookup["COMMANDS_MAG_DESCRIPTION"];
Alias = "mag";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_1"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_2"],
Required = true
}
};
_logger = logger;
_defaultSettings = defaultSettings;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var match = Regex.Match(gameEvent.Data.Trim(), ArgumentRegexPattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (!match.Success)
{
gameEvent.Origin.Tell(Syntax);
return;
}
var map = match.Groups[1].Length > 0 ? match.Groups[1].ToString() : match.Groups[2].ToString();
var gametype = match.Groups[3].Length > 0 ? match.Groups[3].ToString() : match.Groups[4].ToString();
var matchingMaps = gameEvent.Owner.FindMap(map);
var matchingGametypes = _defaultSettings.FindGametype(gametype, gameEvent.Owner.GameName);
if (matchingMaps.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_MAPS"]);
foreach (var matchingMap in matchingMaps)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingMap.Alias}(Color::White)] [(Color::Yellow){matchingMap.Name}(Color::White)]");
}
return;
}
if (matchingGametypes.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_GAMETYPES"]);
foreach (var matchingGametype in matchingGametypes)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingGametype.Alias}(Color::White)] [(Color::Yellow){matchingGametype.Name}(Color::White)]");
}
return;
}
map = matchingMaps.FirstOrDefault()?.Name ?? map;
gametype = matchingGametypes.FirstOrDefault()?.Name ?? gametype;
var hasMatchingGametype = matchingGametypes.Any();
_logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype);
await gameEvent.Owner.SetDvarAsync("g_gametype", gametype, gameEvent.Owner.Manager.CancellationToken);
gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map));
await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds);
switch (gameEvent.Owner.GameName)
{
case Server.Game.IW5:
await gameEvent.Owner.ExecuteCommandAsync(
$"load_dsr {(hasMatchingGametype ? gametype.ToUpper() + "_default" : gametype)}");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
case Server.Game.T6:
await gameEvent.Owner.ExecuteCommandAsync($"exec {gametype}.cfg");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
default:
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
}
}
}
}

View File

@ -1,135 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class OfflineMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{
Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
_contextFactory = contextFactory;
_logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data.Length > MaxLength)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.IsIngame)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
ServerId = server.Id,
Message = gameEvent.Data,
};
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try
{
context.Set<EFInboxMessage>().Add(newMessage);
await context.SaveChangesAsync();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SUCCESS"]);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save offline message {@Message}", newMessage);
throw;
}
}
}
}

View File

@ -1,45 +0,0 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Sends a private message to another player
/// </summary>
public class PrivateMessageCommand : Command
{
public PrivateMessageCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "privatemessage";
Description = _translationLookup["COMMANDS_PM_DESC"];
Alias = "pm";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Target.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data));
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_RESULT"]
.FormatExt(gameEvent.Target.Name, gameEvent.Data));
return Task.CompletedTask;
}
}
}

View File

@ -1,79 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class ReadMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public ReadMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
{
Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm";
Permission = EFClient.Permission.Flagged;
_contextFactory = contextFactory;
_logger = logger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
try
{
await using var context = _contextFactory.CreateContext();
var inboxItems = await context.InboxMessages
.Include(message => message.SourceClient)
.ThenInclude(client => client.CurrentAlias)
.Where(message => message.DestinationClientId == gameEvent.Origin.ClientId)
.Where(message => !message.IsDelivered)
.ToListAsync();
if (!inboxItems.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_NONE"]);
return;
}
var index = 1;
foreach (var inboxItem in inboxItems)
{
await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
.WaitAsync();
foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
{
await gameEvent.Origin.Tell(messageFragment).WaitAsync();
}
index++;
}
inboxItems.ForEach(item => { item.IsDelivered = true; });
context.UpdateRange(inboxItems);
await context.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Could not retrieve offline messages for {Client}", gameEvent.Origin.ToString());
throw;
}
}
}
}

View File

@ -1,77 +0,0 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Report client for given reason
/// </summary>
public class ReportClientCommand : Command
{
public ReportClientCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "report";
Description = _translationLookup["COMMANDS_REPORT_DESC"];
Alias = "rep";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent commandEvent)
{
if (commandEvent.Data.ToLower().Contains("camp"))
{
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_CAMP"]);
return;
}
var success = false;
switch ((await commandEvent.Target.Report(commandEvent.Data, commandEvent.Origin)
.WaitAsync(Utilities.DefaultCommandTimeout, commandEvent.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_SUCCESS"]);
success = true;
break;
case GameEvent.EventFailReason.Exception:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_DUPLICATE"]);
break;
case GameEvent.EventFailReason.Permission:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL"]
.FormatExt(commandEvent.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_SELF"]);
break;
case GameEvent.EventFailReason.Throttle:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_TOOMANY"]);
break;
}
if (success)
{
commandEvent.Owner.ToAdmins(
$"(Color::Accent){commandEvent.Origin.Name}(Color::White) -> (Color::Red){commandEvent.Target.Name}(Color::White): {commandEvent.Data}");
}
}
}
}

View File

@ -1,46 +0,0 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on all servers
/// </summary>
public class SayAllCommand : Command
{
public SayAllCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "sayall";
Description = _translationLookup["COMMANDS_SAY_ALL_DESC"];
Alias = "sa";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = $"(Color::Accent){gameEvent.Origin.Name}(Color::White) - (Color::Red){gameEvent.Data}";
foreach (var server in gameEvent.Owner.Manager.GetServers())
{
server.Broadcast(message, gameEvent.Origin);
}
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -1,42 +0,0 @@
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on the server
/// </summary>
public class SayCommand : Command
{
public SayCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "say";
Description = _translationLookup["COMMANDS_SAY_DESC"];
Alias = "s";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Owner.Broadcast(
_translationLookup["COMMANDS_SAY_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data),
gameEvent.Origin);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -1,38 +0,0 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints client information
/// </summary>
public class WhoAmICommand : Command
{
public WhoAmICommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "whoami";
Description = _translationLookup["COMMANDS_WHO_DESC"];
Alias = "who";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var you =
"[(Color::Yellow)#{{clientNumber}}(Color::White)] [(Color::Yellow)@{{clientId}}(Color::White)] [{{networkId}}] [{{ip}}] [(Color::Cyan){{level}}(Color::White){{tag}}(Color::White)] {{name}}"
.FormatExt(gameEvent.Origin.ClientNumber,
gameEvent.Origin.ClientId, gameEvent.Origin.GuidString,
gameEvent.Origin.IPAddressString, gameEvent.Origin.ClientPermission.Name,
string.IsNullOrEmpty(gameEvent.Origin.Tag) ? "" : $" {gameEvent.Origin.Tag}",
gameEvent.Origin.Name);
gameEvent.Origin.Tell(you);
return Task.CompletedTask;
}
}
}

View File

@ -1,56 +0,0 @@
{
"Serilog": {
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "Log/IW4MAdmin-Application.log",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}",
"RestrictedToMinimumLevel": "Fatal"
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
],
"Destructure": [
{
"Name": "ToMaximumDepth",
"Args": {
"maximumDestructuringDepth": 4
}
},
{
"Name": "ToMaximumStringLength",
"Args": {
"maximumStringLength": 1000
}
},
{
"Name": "ToMaximumCollectionCount",
"Args": {
"maximumCollectionCount": 24
}
}
]
}
}

View File

@ -1,15 +0,0 @@
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Configuration
{
public class ScriptPluginConfiguration : Dictionary<string, Dictionary<string, object>>, IBaseConfiguration
{
public string Name() => nameof(ScriptPluginConfiguration);
public IBaseConfiguration Generate()
{
return new ScriptPluginConfiguration();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,7 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -17,8 +14,6 @@ namespace IW4MAdmin.Application.EventParsers
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations; private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{ {
@ -49,15 +44,8 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.JoinTeam.Pattern = @"^(JT);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(\w+);(.+)$";
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginTeam, 4);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginName, 5);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -72,7 +60,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13); Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -87,30 +75,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13); Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.MapChange.Pattern = @".*InitGame.*";
Configuration.MapEnd.Pattern = @".*(?:ExitLevel|ShutdownGame).*";
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )"; Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
_regexMap = new Dictionary<ParserRegex, GameEvent.EventType>
{
{Configuration.Say, GameEvent.EventType.Say},
{Configuration.Kill, GameEvent.EventType.Kill},
{Configuration.MapChange, GameEvent.EventType.MapChange},
{Configuration.MapEnd, GameEvent.EventType.MapEnd},
{Configuration.JoinTeam, GameEvent.EventType.JoinTeam}
};
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
{
{"say", GameEvent.EventType.Say},
{"sayteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect},
{"JT", GameEvent.EventType.JoinTeam},
{"Q", GameEvent.EventType.PreDisconnect}
};
} }
public IEventParserConfiguration Configuration { get; set; } public IEventParserConfiguration Configuration { get; set; }
@ -123,79 +88,47 @@ namespace IW4MAdmin.Application.EventParsers
public string Name { get; set; } = "Call of Duty"; public string Name { get; set; } = "Call of Duty";
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
{
var lineSplit = logLine.Split(';');
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)
{
var result = key.PatternMatcher.Match(logLine);
if (result.Success)
{
return (value, null);
}
}
return (GameEvent.EventType.Unknown, null);
}
public virtual GameEvent GenerateGameEvent(string logLine) public virtual GameEvent GenerateGameEvent(string logLine)
{ {
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine); var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
var gameTime = 0L; int gameTime = 0;
if (timeMatch.Success) if (timeMatch.Success)
{ {
if (timeMatch.Values[0].Contains(":")) gameTime = timeMatch
{ .Values
gameTime = timeMatch .Skip(2)
.Values // this converts the timestamp into seconds passed
.Skip(2) .Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
// this converts the timestamp into seconds passed .Sum();
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line // we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length).Trim(); logLine = logLine.Substring(timeMatch.Values.First().Length);
} }
var eventParseResult = GetEventTypeFromLine(logLine); string[] lineSplit = logLine.Split(';');
var eventType = eventParseResult.type; string eventType = lineSplit[0];
_logger.LogDebug(logLine);
if (eventType == GameEvent.EventType.Say) if (eventType == "say" || eventType == "sayteam")
{ {
var matchResult = Configuration.Say.PatternMatcher.Match(logLine); var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success) if (matchResult.Success)
{ {
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]] string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "") .Replace("\x15", "")
.Trim(); .Trim();
if (message.Length > 0) if (message.Length > 0)
{ {
var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]; string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var originId = originIdString.IsBotGuid() ? long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix)) if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{ {
@ -227,26 +160,26 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == GameEvent.EventType.Kill) if (eventType == "K")
{ {
var match = Configuration.Kill.PatternMatcher.Match(logLine); var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]]; string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]; string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]; string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
var originId = originIdString.IsBotGuid() ? long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid() ? long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() : targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
@ -261,26 +194,26 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == GameEvent.EventType.Damage) if (eventType == "D")
{ {
var match = Configuration.Damage.PatternMatcher.Match(logLine); var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]]; string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]; string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]; string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
var originId = originIdString.IsBotGuid() ? long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid() ? long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() : targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
@ -295,16 +228,16 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == GameEvent.EventType.PreConnect) if (eventType == "J")
{ {
var match = Configuration.Join.PatternMatcher.Match(logLine); var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]; string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var networkId = originIdString.IsBotGuid() ? long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -316,10 +249,10 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(), Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
}, },
NetworkId = networkId, NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting, State = EFClient.ClientState.Connecting,
}, },
Extra = originIdString, Extra = originIdString,
@ -331,57 +264,16 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == GameEvent.EventType.JoinTeam) if (eventType == "Q")
{
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]];
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Connected,
},
Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.PreDisconnect)
{ {
var match = Configuration.Quit.PatternMatcher.Match(logLine); var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]; string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var networkId = originIdString.IsBotGuid() ? long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -393,10 +285,10 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine() Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
}, },
NetworkId = networkId, NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]), ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting State = EFClient.ClientState.Disconnecting
}, },
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
@ -407,7 +299,7 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == GameEvent.EventType.MapEnd) if (eventType.Contains("ExitLevel"))
{ {
return new GameEvent() return new GameEvent()
{ {
@ -421,9 +313,9 @@ namespace IW4MAdmin.Application.EventParsers
}; };
} }
if (eventType == GameEvent.EventType.MapChange) if (eventType.Contains("InitGame"))
{ {
var dump = logLine.Replace("InitGame: ", ""); string dump = eventType.Replace("InitGame: ", "");
return new GameEvent() return new GameEvent()
{ {
@ -438,37 +330,26 @@ namespace IW4MAdmin.Application.EventParsers
}; };
} }
if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey)) if (_customEventRegistrations.ContainsKey(eventType))
{ {
return new GameEvent() var eventModifier = _customEventRegistrations[eventType];
try
{ {
Type = GameEvent.EventType.Unknown, return eventModifier.Item2(logLine, Configuration, new GameEvent()
Data = logLine, {
Origin = Utilities.IW4MAdminClient(), Type = GameEvent.EventType.Other,
Target = Utilities.IW4MAdminClient(), Data = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.None, Subtype = eventModifier.Item1,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log Source = GameEvent.EventSource.Log
}; });
} }
var eventModifier = _customEventRegistrations[eventParseResult.eventKey]; catch (Exception e)
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{ {
Type = GameEvent.EventType.Other, _logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}");
Data = logLine, }
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception e)
{
_logger.LogError(e, "Could not handle custom event generation");
} }
return new GameEvent() return new GameEvent()

View File

@ -1,6 +1,5 @@
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {

View File

@ -1,8 +1,5 @@
using System.Collections.Generic; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces;
using System.Globalization; using System.Globalization;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -10,35 +7,27 @@ namespace IW4MAdmin.Application.EventParsers
/// generic implementation of the IEventParserConfiguration /// generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic configurations /// allows script plugins to generate dynamic configurations
/// </summary> /// </summary>
internal sealed class DynamicEventParserConfiguration : IEventParserConfiguration sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
{ {
public string GameDirectory { get; set; } public string GameDirectory { get; set; }
public ParserRegex Say { get; set; } public ParserRegex Say { get; set; }
public ParserRegex Join { get; set; } public ParserRegex Join { get; set; }
public ParserRegex JoinTeam { get; set; }
public ParserRegex Quit { get; set; } public ParserRegex Quit { get; set; }
public ParserRegex Kill { get; set; } public ParserRegex Kill { get; set; }
public ParserRegex Damage { get; set; } public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; } public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; } public ParserRegex Time { get; set; }
public ParserRegex MapChange { get; set; }
public ParserRegex MapEnd { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public Dictionary<string, EFClient.TeamType> TeamMapping { get; set; } = new();
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {
Say = parserRegexFactory.CreateParserRegex(); Say = parserRegexFactory.CreateParserRegex();
Join = parserRegexFactory.CreateParserRegex(); Join = parserRegexFactory.CreateParserRegex();
JoinTeam = parserRegexFactory.CreateParserRegex();
Quit = parserRegexFactory.CreateParserRegex(); Quit = parserRegexFactory.CreateParserRegex();
Kill = parserRegexFactory.CreateParserRegex(); Kill = parserRegexFactory.CreateParserRegex();
Damage = parserRegexFactory.CreateParserRegex(); Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex(); Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex(); Time = parserRegexFactory.CreateParserRegex();
MapChange = parserRegexFactory.CreateParserRegex();
MapEnd = parserRegexFactory.CreateParserRegex();
} }
} }
} }

View File

@ -1,10 +1,6 @@
using System; using IW4MAdmin.Application.Misc;
using System.Collections.Generic;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Extensions namespace IW4MAdmin.Application.Extensions
{ {
@ -17,19 +13,9 @@ namespace IW4MAdmin.Application.Extensions
/// <returns></returns> /// <returns></returns>
public static string CommandConfigNameForType(this IManagerCommand command) public static string CommandConfigNameForType(this IManagerCommand command)
{ {
return command.GetType() == typeof(ScriptCommand) return command.GetType() == typeof(ScriptCommand) ?
? $"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command" $"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command" :
: command.GetType().Name; command.GetType().Name;
} }
public static IList<Map> FindMap(this Server server, string mapName) => server.Maps.Where(map =>
map.Name.Equals(mapName, StringComparison.InvariantCultureIgnoreCase) ||
map.Alias.Equals(mapName, StringComparison.InvariantCultureIgnoreCase)).ToList();
public static IList<Gametype> FindGametype(this DefaultSettings settings, string gameType, Server.Game? game = null) =>
settings.Gametypes?.Where(gt => game == null || gt.Game == game)
.SelectMany(gt => gt.Gametypes).Where(gt =>
gt.Alias.Contains(gameType, StringComparison.CurrentCultureIgnoreCase) ||
gt.Name.Contains(gameType, StringComparison.CurrentCultureIgnoreCase)).ToList();
} }
} }

View File

@ -1,106 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Data.MigrationContext;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using ILogger = Serilog.ILogger;
namespace IW4MAdmin.Application.Extensions
{
public static class StartupExtensions
{
private static ILogger _defaultLogger = null;
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
if (_defaultLogger == null)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(Path.Join(Utilities.OperatingDirectory, "Configuration", "LoggingConfiguration.json"))
.Build();
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug();
}
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true));
return services;
}
public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
var activeProvider = appConfig.DatabaseProvider?.ToLower();
if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
{
var currentPath = Utilities.OperatingDirectory;
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"{Path.DirectorySeparatorChar}{currentPath}"
: currentPath;
var connectionStringBuilder = new SqliteConnectionStringBuilder
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
var connectionString = connectionStringBuilder.ToString();
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<SqliteDatabaseContext>()
.UseSqlite(connectionString)
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>())
.EnableSensitiveDataLogging().Options);
return services;
}
switch (activeProvider)
{
case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase);
var connectionString =
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : "");
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
mysqlOptions => mysqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
case "postgresql":
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddSingleton(sp =>
(DbContextOptions) new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions =>
{
postgresqlOptions.EnableRetryOnFailure();
postgresqlOptions.SetPostgresVersion(new Version("12.9"));
})
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
default:
throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Threading.Tasks; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
@ -18,17 +17,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
{ {
var handler = new BaseConfigurationHandler<T>(name); return new BaseConfigurationHandler<T>(name);
handler.BuildAsync().Wait();
return handler;
}
/// <inheritdoc/>
public async Task<IConfigurationHandler<T>> GetConfigurationHandlerAsync<T>(string name) where T : IBaseConfiguration
{
var handler = new BaseConfigurationHandler<T>(name);
await handler.BuildAsync();
return handler;
} }
} }
} }

View File

@ -1,9 +1,5 @@
using System; using SharedLibraryCore.Database;
using Data.Abstractions; using SharedLibraryCore.Interfaces;
using Data.Context;
using Data.MigrationContext;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -12,15 +8,6 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory public class DatabaseContextFactory : IDatabaseContextFactory
{ {
private readonly DbContextOptions _contextOptions;
private readonly string _activeProvider;
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
{
_contextOptions = contextOptions;
_activeProvider = appConfig.DatabaseProvider?.ToLower();
}
/// <summary> /// <summary>
/// creates a new database context /// creates a new database context
/// </summary> /// </summary>
@ -28,35 +15,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true) public DatabaseContext CreateContext(bool? enableTracking = true)
{ {
var context = BuildContext(); return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext();
enableTracking ??= true;
if (enableTracking.Value)
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.ChangeTracker.LazyLoadingEnabled = true;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
}
else
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
context.ChangeTracker.LazyLoadingEnabled = false;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
return context;
}
private DatabaseContext BuildContext()
{
return _activeProvider switch
{
"sqlite" => new SqliteDatabaseContext(_contextOptions),
"mysql" => new MySqlDatabaseContext(_contextOptions),
"postgresql" => new PostgresqlDatabaseContext(_contextOptions),
_ => throw new ArgumentException($"No context found for {_activeProvider}")
};
} }
} }
} }

View File

@ -2,7 +2,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -18,22 +17,14 @@ namespace IW4MAdmin.Application.Factories
public IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser) public IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser)
{ {
var baseUri = logUris[0]; var baseUri = logUris[0];
if (baseUri.Scheme == Uri.UriSchemeHttp || baseUri.Scheme == Uri.UriSchemeHttps) if (baseUri.Scheme == Uri.UriSchemeHttp)
{ {
return new GameLogReaderHttp(logUris, eventParser, return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService<ILogger>());
_serviceProvider.GetRequiredService<ILogger<GameLogReaderHttp>>());
} }
if (baseUri.Scheme == Uri.UriSchemeFile) else if (baseUri.Scheme == Uri.UriSchemeFile)
{ {
return new GameLogReader(baseUri.LocalPath, eventParser, return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService<ILogger>());
_serviceProvider.GetRequiredService<ILogger<GameLogReader>>());
}
if (baseUri.Scheme == Uri.UriSchemeNetTcp)
{
return new NetworkGameLogReader(logUris, eventParser,
_serviceProvider.GetRequiredService<ILogger<NetworkGameLogReader>>());
} }
throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\""); throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\"");

View File

@ -1,10 +1,7 @@
using System; using SharedLibraryCore;
using Data.Abstractions;
using Data.Models.Server;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Collections;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -14,21 +11,21 @@ namespace IW4MAdmin.Application.Factories
internal class GameServerInstanceFactory : IGameServerInstanceFactory internal class GameServerInstanceFactory : IGameServerInstanceFactory
{ {
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaServiceV2 _metaService; private readonly IRConConnectionFactory _rconConnectionFactory;
private readonly IServiceProvider _serviceProvider; private readonly IGameLogReaderFactory _gameLogReaderFactory;
private readonly IMetaService _metaService;
/// <summary> /// <summary>
/// base constructor /// base constructor
/// </summary> /// </summary>
/// <param name="translationLookup"></param> /// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param> /// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService)
IMetaServiceV2 metaService,
IServiceProvider serviceProvider)
{ {
_translationLookup = translationLookup; _translationLookup = translationLookup;
_rconConnectionFactory = rconConnectionFactory;
_gameLogReaderFactory = gameLogReaderFactory;
_metaService = metaService; _metaService = metaService;
_serviceProvider = serviceProvider;
} }
/// <summary> /// <summary>
@ -39,10 +36,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager) public Server CreateServer(ServerConfiguration config, IManager manager)
{ {
return new IW4MServer(config, return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService);
_serviceProvider.GetRequiredService<CommandConfiguration>(), _translationLookup, _metaService,
_serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(),
_serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
} }
} }
} }

View File

@ -1,13 +1,6 @@
using System; using IW4MAdmin.Application.RCon;
using System.Net;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Text; using System.Text;
using Integrations.Cod;
using Integrations.Source;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -16,31 +9,28 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
internal class RConConnectionFactory : IRConConnectionFactory internal class RConConnectionFactory : IRConConnectionFactory
{ {
private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252"); private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger;
/// <summary> /// <summary>
/// Base constructor /// Base constructor
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger"></param>
public RConConnectionFactory(IServiceProvider serviceProvider) public RConConnectionFactory(ILogger logger)
{ {
_serviceProvider = serviceProvider; _logger = logger;
} }
/// <inheritdoc/> /// <summary>
public IRConConnection CreateConnection(IPEndPoint ipEndpoint, string password, string rconEngine) /// creates a new rcon connection instance
/// </summary>
/// <param name="ipAddress">ip address of the server</param>
/// <param name="port">port of the server</param>
/// <param name="password">rcon password of the server</param>
/// <returns></returns>
public IRConConnection CreateConnection(string ipAddress, int port, string password)
{ {
return rconEngine switch return new RConConnection(ipAddress, port, password, _logger, gameEncoding);
{
"COD" => new CodRConConnection(ipEndpoint, password,
_serviceProvider.GetRequiredService<ILogger<CodRConConnection>>(), GameEncoding,
_serviceProvider.GetRequiredService<ApplicationConfiguration>()?.ServerConnectionAttempts ?? 6),
"Source" => new SourceRConConnection(
_serviceProvider.GetRequiredService<ILogger<SourceRConConnection>>(),
_serviceProvider.GetRequiredService<IRConClientFactory>(), ipEndpoint, password),
_ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'")
};
} }
} }
} }

View File

@ -6,10 +6,7 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -18,30 +15,26 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
public class ScriptCommandFactory : IScriptCommandFactory public class ScriptCommandFactory : IScriptCommandFactory
{ {
private readonly CommandConfiguration _config; private CommandConfiguration _config;
private readonly ITranslationLookup _transLookup; private readonly ITranslationLookup _transLookup;
private readonly IServiceProvider _serviceProvider;
public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup, IServiceProvider serviceProvider) public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup)
{ {
_config = config; _config = config;
_transLookup = transLookup; _transLookup = transLookup;
_serviceProvider = serviceProvider;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
{ {
var permissionEnum = Enum.Parse<EFClient.Permission>(permission); var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
{ {
Name = _arg.Item1, Name = _arg.Item1,
Required = _arg.Item2 Required = _arg.Item2
}).ToArray(); }).ToArray();
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
} }
} }
} }

View File

@ -1,19 +1,19 @@
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
public class GameEventHandler : IEventHandler public class GameEventHandler : IEventHandler
{ {
private readonly EventLog _eventLog; private readonly EventLog _eventLog;
private readonly ILogger _logger;
private readonly IEventPublisher _eventPublisher;
private static readonly GameEvent.EventType[] overrideEvents = new[] private static readonly GameEvent.EventType[] overrideEvents = new[]
{ {
GameEvent.EventType.Connect, GameEvent.EventType.Connect,
@ -22,25 +22,34 @@ namespace IW4MAdmin.Application
GameEvent.EventType.Stop GameEvent.EventType.Stop
}; };
public GameEventHandler(ILogger<GameEventHandler> logger, IEventPublisher eventPublisher) public GameEventHandler()
{ {
_eventLog = new EventLog(); _eventLog = new EventLog();
_logger = logger;
_eventPublisher = eventPublisher;
} }
public void HandleEvent(IManager manager, GameEvent gameEvent) public void HandleEvent(IManager manager, GameEvent gameEvent)
{ {
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type)) if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{ {
#if DEBUG
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
#endif
EventApi.OnGameEvent(gameEvent); EventApi.OnGameEvent(gameEvent);
_eventPublisher.Publish(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent)); Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
} }
#if DEBUG
else else
{ {
_logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id); gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}");
} }
#endif
} }
} }
} }

View File

@ -3,9 +3,6 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -15,14 +12,12 @@ namespace IW4MAdmin.Application.IO
private readonly Server _server; private readonly Server _server;
private readonly IGameLogReader _reader; private readonly IGameLogReader _reader;
private readonly bool _ignoreBots; private readonly bool _ignoreBots;
private readonly ILogger _logger;
public GameLogEventDetection(ILogger<GameLogEventDetection> logger, IW4MServer server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory) public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{ {
_reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser); _reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
_server = server; _server = server;
_ignoreBots = server.Manager.GetApplicationSettings().Configuration()?.IgnoreBots ?? false; _ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
_logger = logger;
} }
public async Task PollForChanges() public async Task PollForChanges()
@ -38,17 +33,15 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
using(LogContext.PushProperty("Server", _server.ToString())) _server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
{ _server.Logger.WriteDebug(e.GetExceptionInfo());
_logger.LogError(e, "Failed to update log event for {endpoint}", _server.EndPoint);
}
} }
} }
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken); await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
} }
_logger.LogDebug("Stopped polling for changes"); _server.Logger.WriteDebug("Stopped polling for changes");
} }
public async Task UpdateLogEvents() public async Task UpdateLogEvents()
@ -69,12 +62,15 @@ namespace IW4MAdmin.Application.IO
return; return;
} }
var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize, _server); var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize);
foreach (var gameEvent in events) foreach (var gameEvent in events)
{ {
try try
{ {
#if DEBUG
_server.Logger.WriteVerbose(gameEvent.Data);
#endif
gameEvent.Owner = _server; gameEvent.Owner = _server;
// we don't want to add the event if ignoreBots is on and the event comes from a bot // we don't want to add the event if ignoreBots is on and the event comes from a bot
@ -106,14 +102,10 @@ namespace IW4MAdmin.Application.IO
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
if (_ignoreBots) if (!_ignoreBots)
{ {
continue; _server.Logger.WriteWarning("Could not find client in client list when parsing event line");
} _server.Logger.WriteDebug(gameEvent.Data);
using(LogContext.PushProperty("Server", _server.ToString()))
{
_logger.LogError("Could not find client in client list when parsing event line {data}", gameEvent.Data);
} }
} }
} }

View File

@ -6,8 +6,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -21,14 +19,14 @@ namespace IW4MAdmin.Application.IO
public int UpdateInterval => 300; public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser, ILogger<GameLogReader> logger) public GameLogReader(string logFile, IEventParser parser, ILogger logger)
{ {
_logFile = logFile; _logFile = logFile;
_parser = parser; _parser = parser;
_logger = logger; _logger = logger;
} }
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition, Server server = null) public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition)
{ {
// allocate the bytes for the new log lines // allocate the bytes for the new log lines
List<string> logLines = new List<string>(); List<string> logLines = new List<string>();
@ -75,7 +73,9 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Could not properly parse event line {@eventLine}", eventLine); _logger.WriteWarning("Could not properly parse event line");
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
} }
} }

View File

@ -6,8 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -22,7 +20,7 @@ namespace IW4MAdmin.Application.IO
private readonly string _safeLogPath; private readonly string _safeLogPath;
private string lastKey = "next"; private string lastKey = "next";
public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger<GameLogReaderHttp> logger) public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger)
{ {
_eventParser = parser; _eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString()); _logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString());
@ -34,7 +32,7 @@ namespace IW4MAdmin.Application.IO
public int UpdateInterval => 500; public int UpdateInterval => 500;
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition, Server server = null) public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition)
{ {
var events = new List<GameEvent>(); var events = new List<GameEvent>();
var response = await _logServerApi.Log(_safeLogPath, lastKey); var response = await _logServerApi.Log(_safeLogPath, lastKey);
@ -42,7 +40,7 @@ namespace IW4MAdmin.Application.IO
if (!response.Success && string.IsNullOrEmpty(lastKey)) if (!response.Success && string.IsNullOrEmpty(lastKey))
{ {
_logger.LogError("Could not get log server info of {logPath}", _safeLogPath); _logger.WriteError($"Could not get log server info of {_safeLogPath}");
return events; return events;
} }
@ -64,7 +62,9 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Could not properly parse event line from http {eventLine}", eventLine); _logger.WriteError("Could not properly parse event line from http");
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
} }
} }
} }

View File

@ -1,163 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Integrations.Cod;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capability of reading log files over udp
/// </summary>
class NetworkGameLogReader : IGameLogReader
{
private readonly IEventParser _eventParser;
private readonly ILogger _logger;
private readonly Uri _uri;
private static readonly NetworkLogState State = new();
private bool _stateRegistered;
private CancellationToken _token;
public NetworkGameLogReader(IReadOnlyList<Uri> uris, IEventParser parser, ILogger<NetworkGameLogReader> logger)
{
_eventParser = parser;
_uri = uris[0];
_logger = logger;
}
public long Length => -1;
public int UpdateInterval => 150;
public Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition,
Server server = null)
{
// todo: other games might support this
var serverEndpoint = (server?.RemoteConnection as CodRConConnection)?.Endpoint;
if (serverEndpoint is null)
{
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
if (!_stateRegistered && !State.EndPointExists(serverEndpoint))
{
try
{
var client = State.RegisterEndpoint(serverEndpoint, BuildLocalEndpoint()).Client;
_stateRegistered = true;
_token = server.Manager.CancellationToken;
if (client == null)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogInformation("Not registering {Name} socket because it is already bound",
nameof(NetworkGameLogReader));
}
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
Task.Run(async () => await ReadNetworkData(client, _token), _token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not register {Name} endpoint {Endpoint}",
nameof(NetworkGameLogReader), _uri);
throw;
}
}
var events = new List<GameEvent>();
foreach (var logData in State.GetServerLogData(serverEndpoint)
.Select(log => Utilities.EncodingType.GetString(log)))
{
if (string.IsNullOrWhiteSpace(logData))
{
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
var lines = logData
.Split('\n')
.Where(line => line.Length > 0 && !line.Contains('ÿ'));
foreach (var eventLine in lines)
{
try
{
// this trim end should hopefully fix the nasty runaway regex
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
events.Add(gameEvent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not properly parse event line from http {EventLine}",
eventLine);
}
}
}
return Task.FromResult((IEnumerable<GameEvent>)events);
}
private async Task ReadNetworkData(UdpClient client, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// get more data
IPEndPoint remoteEndpoint = null;
byte[] bufferedData = null;
if (client == null)
{
// we already have a socket listening on this port for data, so we don't need to run another thread
break;
}
try
{
var result = await client.ReceiveAsync(_token);
remoteEndpoint = result.RemoteEndPoint;
bufferedData = result.Buffer;
}
catch (OperationCanceledException)
{
_logger.LogDebug("Stopping network log receive");
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not receive lines for {LogReader}", nameof(NetworkGameLogReader));
}
if (bufferedData != null)
{
State.QueueServerLogData(remoteEndpoint, bufferedData);
}
}
}
private IPEndPoint BuildLocalEndpoint()
{
try
{
return new IPEndPoint(Dns.GetHostAddresses(_uri.Host).First(), _uri.Port);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not setup {LogReader} endpoint", nameof(NetworkGameLogReader));
throw;
}
}
}
}

View File

@ -1,138 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace IW4MAdmin.Application.IO;
public class NetworkLogState : Dictionary<IPEndPoint, UdpClientState>
{
public UdpClientState RegisterEndpoint(IPEndPoint serverEndpoint, IPEndPoint localEndpoint)
{
try
{
lock (this)
{
if (!ContainsKey(serverEndpoint))
{
Add(serverEndpoint, new UdpClientState { Client = new UdpClient(localEndpoint) });
}
}
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
lock (this)
{
// we don't add the udp client because it already exists (listening to multiple servers from one socket)
Add(serverEndpoint, new UdpClientState());
}
}
return this[serverEndpoint];
}
public List<byte[]> GetServerLogData(IPEndPoint serverEndpoint)
{
try
{
var state = this[serverEndpoint];
if (state == null)
{
return new List<byte[]>();
}
// it's possible that we could be trying to read and write to the queue simultaneously so we need to wait
this[serverEndpoint].OnAction.Wait();
var data = new List<byte[]>();
while (this[serverEndpoint].AvailableLogData.Count > 0)
{
data.Add(this[serverEndpoint].AvailableLogData.Dequeue());
}
return data;
}
finally
{
if (this[serverEndpoint].OnAction.CurrentCount == 0)
{
this[serverEndpoint].OnAction.Release(1);
}
}
}
public void QueueServerLogData(IPEndPoint serverEndpoint, byte[] data)
{
var endpoint = Keys.FirstOrDefault(key =>
Equals(key.Address, serverEndpoint.Address) && key.Port == serverEndpoint.Port);
try
{
if (endpoint == null)
{
return;
}
// currently our expected start and end characters
var startsWithPrefix = StartsWith(data, "ÿÿÿÿprint\n");
var endsWithDelimiter = data[^1] == '\n';
// we have the data we expected
if (!startsWithPrefix || !endsWithDelimiter)
{
return;
}
// it's possible that we could be trying to read and write to the queue simultaneously so we need to wait
this[endpoint].OnAction.Wait();
this[endpoint].AvailableLogData.Enqueue(data);
}
finally
{
if (endpoint != null && this[endpoint].OnAction.CurrentCount == 0)
{
this[endpoint].OnAction.Release(1);
}
}
}
public bool EndPointExists(IPEndPoint serverEndpoint)
{
lock (this)
{
return ContainsKey(serverEndpoint);
}
}
private static bool StartsWith(byte[] sourceArray, string match)
{
if (sourceArray is null)
{
return false;
}
if (match.Length > sourceArray.Length)
{
return false;
}
return !match.Where((t, i) => sourceArray[i] != (byte)t).Any();
}
}
public class UdpClientState
{
public UdpClient Client { get; set; }
public Queue<byte[]> AvailableLogData { get; } = new();
public SemaphoreSlim OnAction { get; } = new(1, 1);
~UdpClientState()
{
OnAction.Dispose();
Client?.Dispose();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,22 +6,15 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Localization namespace IW4MAdmin.Application.Localization
{ {
public static class Configure public class Configure
{ {
public static ITranslationLookup Initialize(ILogger logger, IMasterApi apiInstance, ApplicationConfiguration applicationConfiguration) public static ITranslationLookup Initialize(bool useLocalTranslation, IMasterApi apiInstance, string customLocale = null)
{ {
var useLocalTranslation = applicationConfiguration?.UseLocalTranslations ?? true; string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
var customLocale = applicationConfiguration?.EnableCustomLocale ?? false string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
? (applicationConfiguration.CustomLocale ?? "en-US")
: "en-US";
var currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
var localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (!useLocalTranslation) if (!useLocalTranslation)
{ {
@ -32,10 +25,9 @@ namespace IW4MAdmin.Application.Localization
return localization.LocalizationIndex; return localization.LocalizationIndex;
} }
catch (Exception ex) catch (Exception)
{ {
// the online localization failed so will default to local files // the online localization failed so will default to local files
logger.LogWarning(ex, "Could not download latest translations");
} }
} }
@ -63,20 +55,18 @@ namespace IW4MAdmin.Application.Localization
{ {
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents); var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null)
{
continue;
}
foreach (var item in eachLocalizationFile.LocalizationIndex.Set) foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{ {
if (!localizationDict.TryAdd(item.Key, item.Value)) if (!localizationDict.TryAdd(item.Key, item.Value))
{ {
logger.LogError("Could not add locale string {key} to localization", item.Key); Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization");
} }
} }
} }
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict) Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{ {
LocalizationName = currentLocale, LocalizationName = currentLocale,

View File

@ -1,6 +1,7 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories; using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta; using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
@ -17,36 +18,21 @@ using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories; using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using Stats.Dtos; using Stats.Dtos;
using StatsWeb;
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
public class Program public class Program
{ {
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString()); public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager; public static ApplicationManager ServerManager;
private static Task _applicationTask; private static Task ApplicationTask;
private static ServiceProvider _serviceProvider; private static ServiceProvider serviceProvider;
/// <summary> /// <summary>
/// entrypoint of the application /// entrypoint of the application
@ -59,7 +45,7 @@ namespace IW4MAdmin.Application
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray; Console.ForegroundColor = ConsoleColor.Gray;
Console.CancelKeyPress += OnCancelKey; Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.WriteLine("====================================================="); Console.WriteLine("=====================================================");
Console.WriteLine(" IW4MAdmin"); Console.WriteLine(" IW4MAdmin");
@ -78,15 +64,8 @@ namespace IW4MAdmin.Application
/// <param name="e"></param> /// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{ {
if (_serverManager is not null) ServerManager?.Stop();
{ await ApplicationTask;
await _serverManager.Stop();
}
if (_applicationTask is not null)
{
await _applicationTask;
}
} }
/// <summary> /// <summary>
@ -95,47 +74,31 @@ namespace IW4MAdmin.Application
/// <returns></returns> /// <returns></returns>
private static async Task LaunchAsync(string[] args) private static async Task LaunchAsync(string[] args)
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try try
{ {
// do any needed housekeeping file/folder migrations // do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services...");
var services = await ConfigureServices(args);
_serviceProvider = services.BuildServiceProvider();
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
_applicationTask = RunApplicationTasksAsync(logger, services); var services = ConfigureServices(args);
var tasks = new[] serviceProvider = services.BuildServiceProvider();
{ var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
versionChecker.CheckVersion(), ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
_applicationTask translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
};
await _serverManager.Init(); ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
await Task.WhenAll(tasks); await versionChecker.CheckVersion();
await ServerManager.Init();
} }
catch (Exception e) catch (Exception e)
{ {
var failMessage = translationLookup == null string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
? "Failed to initialize IW4MAdmin" string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
: translationLookup["MANAGER_INIT_FAIL"];
var exitMessage = translationLookup == null
? "Press enter to exit..."
: translationLookup["MANAGER_EXIT"];
logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage); Console.WriteLine(failMessage);
while (e.InnerException != null) while (e.InnerException != null)
@ -145,10 +108,12 @@ namespace IW4MAdmin.Application
if (e is ConfigurationException configException) if (e is ConfigurationException configException)
{ {
Console.WriteLine("{{fileName}} contains an error." if (translationLookup != null)
.FormatExt(Path.GetFileName(configException.ConfigurationFileName))); {
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
}
foreach (var error in configException.Errors) foreach (string error in configException.Errors)
{ {
Console.WriteLine(error); Console.WriteLine(error);
} }
@ -159,249 +124,118 @@ namespace IW4MAdmin.Application
Console.WriteLine(e.Message); Console.WriteLine(e.Message);
} }
if (_serverManager is not null)
{
await _serverManager?.Stop();
}
Console.WriteLine(exitMessage); Console.WriteLine(exitMessage);
await Console.In.ReadAsync(new char[1], 0, 1); await Console.In.ReadAsync(new char[1], 0, 1);
return; return;
} }
if (_serverManager.IsRestartRequested) try
{
ApplicationTask = RunApplicationTasksAsync();
await ApplicationTask;
}
catch (Exception e)
{
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
if (ServerManager.IsRestartRequested)
{ {
goto restart; goto restart;
} }
await _serviceProvider.DisposeAsync(); serviceProvider.Dispose();
} }
/// <summary> /// <summary>
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services) private static async Task RunApplicationTasksAsync()
{ {
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken) WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
: Task.CompletedTask; Task.CompletedTask;
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
// we want to run this one on a manual thread instead of letting the thread pool handle it, // we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting // because we can't exit early from waiting on console input, and it prevents us from restarting
async void ReadInput() => await ReadConsoleInput(logger); var inputThread = new Thread(async () => await ReadConsoleInput());
var inputThread = new Thread(ReadInput);
inputThread.Start(); inputThread.Start();
var tasks = new[] var tasks = new[]
{ {
ServerManager.Start(),
webfrontTask, webfrontTask,
_serverManager.Start(), serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken)
_serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
}; };
logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully"); ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
} }
/// <summary> /// <summary>
/// reads input from the console and executes entered commands on the default server /// reads input from the console and executes entered commands on the default server
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task ReadConsoleInput(ILogger logger) private static async Task ReadConsoleInput()
{ {
if (Console.IsInputRedirected) if (Console.IsInputRedirected)
{ {
logger.LogInformation("Disabling console input as it has been redirected"); ServerManager.Logger.WriteInfo("Disabling console input as it has been redirected");
return; return;
} }
EFClient origin = null; string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
try try
{ {
while (!_serverManager.CancellationToken.IsCancellationRequested) while (!ServerManager.CancellationToken.IsCancellationRequested)
{ {
if (!_serverManager.IsInitialized) lastCommand = await Console.In.ReadLineAsync();
if (lastCommand?.Length > 0)
{ {
await Task.Delay(1000); if (lastCommand?.Length > 0)
continue; {
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>');
}
} }
var lastCommand = await Console.In.ReadLineAsync();
if (lastCommand == null)
{
continue;
}
if (!lastCommand.Any())
{
continue;
}
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = origin ??= Utilities.IW4MAdminClient(_serverManager.Servers.FirstOrDefault()),
Owner = _serverManager.Servers[0]
};
_serverManager.AddEvent(gameEvent);
await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, _serverManager.CancellationToken);
Console.Write('>');
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ { }
}
} }
private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
IServiceCollection serviceCollection,
IMasterApi masterApi)
{
var defaultLogger = BuildDefaultLogger<Program>(appConfig);
var pluginServiceProvider = new ServiceCollection()
.AddBaseLogger(appConfig)
.AddSingleton(appConfig)
.AddSingleton(masterApi)
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IPluginImporter, PluginImporter>()
.BuildServiceProvider();
var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
// we need to register the rest client with regular collection
serviceCollection.AddSingleton(masterApi);
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Concat(typeof(Program).Assembly.GetTypes().Where(type => type.Namespace?.StartsWith("IW4MAdmin.Application.Commands") ?? false))
.Where(command => command.BaseType == typeof(Command)))
{
defaultLogger.LogDebug("Registered native command type {Name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
foreach (var commandType in commands)
{
defaultLogger.LogDebug("Registered plugin command type {Name}", commandType.FullName);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
foreach (var configurationType in configurations)
{
defaultLogger.LogDebug("Registered plugin config type {Name}", configurationType.Name);
var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
var handlerInstance = Activator.CreateInstance(handlerType, configInstance.Name());
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
// register any script plugins
foreach (var plugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(plugin);
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))
.Union(plugins.SelectMany(asm => asm.Assembly.GetTypes())
.Distinct()
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection;
}
/// <summary> /// <summary>
/// Configures the dependency injection services /// Configures the dependency injection services
/// </summary> /// </summary>
private static async Task<IServiceCollection> ConfigureServices(string[] args) private static IServiceCollection ConfigureServices(string[] args)
{ {
// todo: this is a quick fix var defaultLogger = new Logger("IW4MAdmin-Manager");
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); var pluginImporter = new PluginImporter(defaultLogger);
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
await appConfigHandler.BuildAsync(); .AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings"); .AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
await defaultConfigHandler.BuildAsync(); .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration())
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration"); .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
await commandConfigHandler.BuildAsync(); .AddSingleton<ILogger>(_serviceProvider => defaultLogger)
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
await statsCommandHandler.BuildAsync();
var defaultConfig = defaultConfigHandler.Configuration();
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var httpClient = new HttpClient
{
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
await appConfigHandler.Save();
}
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
}
// build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig)
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>() .AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>() .AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>() .AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
@ -413,46 +247,27 @@ namespace IW4MAdmin.Application
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>() .AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>() .AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>() .AddSingleton<IEntityService<EFClient>, ClientService>()
#pragma warning disable CS0618
.AddSingleton<IMetaService, MetaService>() .AddSingleton<IMetaService, MetaService>()
#pragma warning restore CS0618
.AddSingleton<IMetaServiceV2, MetaServiceV2>()
.AddSingleton<ClientService>()
.AddSingleton<PenaltyService>()
.AddSingleton<ChangeHistoryService>()
.AddSingleton<IMetaRegistration, MetaRegistration>() .AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>() .AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
ReceivedPenaltyResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()
AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>() .AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>() .AddSingleton(_serviceProvider =>
.AddSingleton<IMasterCommunication, MasterCommunication>() {
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
apiInstance: _serviceProvider.GetRequiredService<IMasterApi>(),
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
})
.AddSingleton<IManager, ApplicationManager>() .AddSingleton<IManager, ApplicationManager>()
#pragma warning disable CS0612 .AddSingleton(_serviceProvider => RestClient
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>() .For<IMasterApi>(Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : _serviceProvider
#pragma warning restore CS0612 .GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration()?.MasterUrl ??
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>() new ApplicationConfiguration().MasterUrl))
.AddSingleton<IClientStatisticCalculator, HitCalculator>() .AddSingleton<IMasterCommunication, MasterCommunication>();
.AddSingleton<IServerDistributionCalculator, ServerDistributionCalculator>()
.AddSingleton<IWeaponNameParser, WeaponNameParser>()
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents")) if (args.Contains("serialevents"))
{ {
@ -463,18 +278,48 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>(); serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
} }
serviceCollection.AddSource(); // register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
{
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1)
{
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}");
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
foreach (var commandType in pluginImplementations.Item2)
{
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin);
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(pluginImplementations
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection; return serviceCollection;
} }
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
{
var collection = new ServiceCollection()
.AddBaseLogger(appConfig)
.BuildServiceProvider();
return collection.GetRequiredService<ILogger<T>>();
}
} }
} }

View File

@ -1,14 +1,11 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -21,7 +18,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger<AdministeredPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory) public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -29,7 +26,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
await using var ctx = _contextFactory.CreateContext(enableTracking: false); using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId) .Where(_penalty => query.ClientId == _penalty.PunisherId)

View File

@ -1,60 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
public class
ConnectionsResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public ConnectionsResourceQueryHelper(ILogger<ConnectionsResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<ConnectionHistoryResponse>> QueryResource(
ClientPaginationRequest query)
{
_logger.LogDebug("{Class} {@Request}", nameof(ConnectionsResourceQueryHelper), query);
await using var context = _contextFactory.CreateContext(enableTracking: false);
var iqConnections = context.ConnectionHistory.AsNoTracking()
.Where(history => query.ClientId == history.ClientId)
.Where(history => history.CreatedDateTime < query.Before)
.OrderByDescending(history => history.CreatedDateTime);
var connections = await iqConnections.Select(history => new ConnectionHistoryResponse
{
MetaId = history.ClientConnectionId,
ClientId = history.ClientId,
Type = MetaType.ConnectionHistory,
ShouldDisplay = true,
When = history.CreatedDateTime,
ServerName = history.Server.HostName,
ConnectionType = history.ConnectionType
})
.ToListAsync();
_logger.LogDebug("{Class} retrieved {Number} items", nameof(ConnectionsResourceQueryHelper),
connections.Count);
return new ResourceQueryHelperResult<ConnectionHistoryResponse>
{
Results = connections
};
}
}
}

View File

@ -5,10 +5,7 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -16,28 +13,16 @@ namespace IW4MAdmin.Application.Meta
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private ITranslationLookup _transLookup; private ITranslationLookup _transLookup;
private readonly IMetaServiceV2 _metaService; private readonly IMetaService _metaService;
private readonly IEntityService<EFClient> _clientEntityService; private readonly IEntityService<EFClient> _clientEntityService;
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
_administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse> public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
_connectionHistoryHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>
_permissionLevelHelper;
public MetaRegistration(ILogger<MetaRegistration> logger, IMetaServiceV2 metaService,
ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper, IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse> connectionHistoryHelper,
IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse> permissionLevelHelper)
{ {
_logger = logger; _logger = logger;
_transLookup = transLookup; _transLookup = transLookup;
@ -46,32 +31,20 @@ namespace IW4MAdmin.Application.Meta
_receivedPenaltyHelper = receivedPenaltyHelper; _receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper; _administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper; _updatedAliasHelper = updatedAliasHelper;
_connectionHistoryHelper = connectionHistoryHelper;
_permissionLevelHelper = permissionLevelHelper;
} }
public void Register() public void Register()
{ {
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetProfileMeta);
GetProfileMeta); _metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, _metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
GetReceivedPenaltiesMeta); _metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized,
GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate,
GetUpdatedAliasMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ConnectionHistoryResponse>(MetaType.ConnectionHistory,
GetConnectionHistoryMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, PermissionLevelChangedResponse>(
MetaType.PermissionLevel, GetPermissionLevelMeta);
} }
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request, private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
CancellationToken cancellationToken = default)
{ {
var metaList = new List<InformationResponse>(); var metaList = new List<InformationResponse>();
var lastMapMeta = var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = request.ClientId });
await _metaService.GetPersistentMeta("LastMapPlayed", request.ClientId, cancellationToken);
if (lastMapMeta != null) if (lastMapMeta != null)
{ {
@ -83,12 +56,12 @@ namespace IW4MAdmin.Application.Meta
Value = lastMapMeta.Value, Value = lastMapMeta.Value,
ShouldDisplay = true, ShouldDisplay = true,
Type = MetaType.Information, Type = MetaType.Information,
Column = 1,
Order = 6 Order = 6
}); });
} }
var lastServerMeta = var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = request.ClientId });
await _metaService.GetPersistentMeta("LastServerPlayed", request.ClientId, cancellationToken);
if (lastServerMeta != null) if (lastServerMeta != null)
{ {
@ -100,7 +73,8 @@ namespace IW4MAdmin.Application.Meta
Value = lastServerMeta.Value, Value = lastServerMeta.Value,
ShouldDisplay = true, ShouldDisplay = true,
Type = MetaType.Information, Type = MetaType.Information,
Order = 7 Column = 0,
Order = 6
}); });
} }
@ -108,7 +82,7 @@ namespace IW4MAdmin.Application.Meta
if (client == null) if (client == null)
{ {
_logger.LogWarning("No client found with id {ClientId} when generating profile meta", request.ClientId); _logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta");
return metaList; return metaList;
} }
@ -118,7 +92,8 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"], Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(), Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Order = 8, Column = 1,
Order = 0,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -128,7 +103,8 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"], Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(), Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Order = 9, Column = 1,
Order = 1,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -138,7 +114,8 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"], Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(), Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Order = 10, Column = 1,
Order = 2,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -146,10 +123,10 @@ namespace IW4MAdmin.Application.Meta
{ {
ClientId = client.ClientId, ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
ShouldDisplay = true, ShouldDisplay = true,
Order = 11, Column = 1,
Order = 3,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -157,50 +134,32 @@ namespace IW4MAdmin.Application.Meta
{ {
ClientId = client.ClientId, ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"]
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
IsSensitive = true, IsSensitive = true,
Order = 12, Column = 1,
Order = 4,
Type = MetaType.Information Type = MetaType.Information
}); });
return metaList; return metaList;
} }
private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta( private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta(ClientPaginationRequest request)
ClientPaginationRequest request, CancellationToken token = default)
{ {
var penalties = await _receivedPenaltyHelper.QueryResource(request); var penalties = await _receivedPenaltyHelper.QueryResource(request);
return penalties.Results; return penalties.Results;
} }
private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta( private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta(ClientPaginationRequest request)
ClientPaginationRequest request, CancellationToken token = default)
{ {
var penalties = await _administeredPenaltyHelper.QueryResource(request); var penalties = await _administeredPenaltyHelper.QueryResource(request);
return penalties.Results; return penalties.Results;
} }
private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request, private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request)
CancellationToken token = default)
{ {
var aliases = await _updatedAliasHelper.QueryResource(request); var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results; return aliases.Results;
} }
private async Task<IEnumerable<ConnectionHistoryResponse>> GetConnectionHistoryMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var connections = await _connectionHistoryHelper.QueryResource(request);
return connections.Results;
}
private async Task<IEnumerable<PermissionLevelChangedResponse>> GetPermissionLevelMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var permissionChanges = await _permissionLevelHelper.QueryResource(request);
return permissionChanges.Results;
}
} }
} }

View File

@ -1,51 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta;
public class
PermissionLevelChangedResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest,
PermissionLevelChangedResponse>
{
private readonly IDatabaseContextFactory _contextFactory;
public PermissionLevelChangedResourceQueryHelper(IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<PermissionLevelChangedResponse>> QueryResource(
ClientPaginationRequest query)
{
await using var context = _contextFactory.CreateContext();
var auditEntries = context.EFChangeHistory.Where(change => change.TargetEntityId == query.ClientId)
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission);
var audits = from change in auditEntries
join client in context.Clients
on change.OriginEntityId equals client.ClientId
select new PermissionLevelChangedResponse
{
ChangedById = change.OriginEntityId,
ChangedByName = client.CurrentAlias.Name,
PreviousPermissionLevelValue = change.PreviousValue,
CurrentPermissionLevelValue = change.CurrentValue,
When = change.TimeChanged,
ClientId = change.TargetEntityId,
IsSensitive = true
};
return new ResourceQueryHelperResult<PermissionLevelChangedResponse>
{
Results = await audits.Skip(query.Offset).Take(query.Count).ToListAsync()
};
}
}

View File

@ -1,18 +1,13 @@
using System.Collections.Generic; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -20,75 +15,33 @@ namespace IW4MAdmin.Application.Meta
/// implementation of IResourceQueryHelper /// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id /// used to pull in penalties applied to a given client id
/// </summary> /// </summary>
public class public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger, public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_appConfig = appConfig;
} }
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource( public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
ClientPaginationRequest query)
{ {
var linkedPenaltyType = Utilities.LinkedPenaltyTypes(); var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
await using var ctx = _contextFactory.CreateContext(enableTracking: false); using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking() var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId) .Where(_client => _client.ClientId == query.ClientId)
.Select(_client => new { _client.AliasLinkId, _client.CurrentAliasId }) .Select(_client => _client.AliasLinkId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId || .Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
IQueryable<EFPenalty> iqIpLinkedPenalties = null;
IQueryable<EFPenalty> identifierPenalties = null;
if (!_appConfig.EnableImplicitAccountLinking)
{
var usedIps = await ctx.Aliases.AsNoTracking()
.Where(alias =>
(alias.LinkId == linkId.AliasLinkId || alias.AliasId == linkId.CurrentAliasId) &&
alias.IPAddress != null)
.Select(alias => alias.IPAddress).ToListAsync();
identifierPenalties = ctx.PenaltyIdentifiers.AsNoTracking().Where(identifier =>
identifier.IPv4Address != null && usedIps.Contains(identifier.IPv4Address))
.Select(id => id.Penalty);
var aliasedIds = await ctx.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress))
.Select(alias => alias.LinkId)
.ToListAsync();
iqIpLinkedPenalties = ctx.Penalties.AsNoTracking()
.Where(penalty =>
linkedPenaltyType.Contains(penalty.Type) && aliasedIds.Contains(penalty.LinkId ?? -1));
}
var iqAllPenalties = iqPenalties;
if (iqIpLinkedPenalties != null)
{
iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
}
if (identifierPenalties != null)
{
iqAllPenalties = iqPenalties.Union(identifierPenalties);
}
var penalties = await iqAllPenalties
.Where(_penalty => _penalty.When < query.Before) .Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When) .OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count) .Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse() .Select(_penalty => new ReceivedPenaltyResponse()
{ {
@ -112,7 +65,7 @@ namespace IW4MAdmin.Application.Meta
{ {
// todo: maybe actually count // todo: maybe actually count
RetrievedResultCount = penalties.Count, RetrievedResultCount = penalties.Count,
Results = penalties.Distinct() Results = penalties
}; };
} }
} }

View File

@ -6,9 +6,6 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -21,7 +18,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public UpdatedAliasResourceQueryHelper(ILogger<UpdatedAliasResourceQueryHelper> logger, IDatabaseContextFactory contextFactory) public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_contextFactory = contextFactory; _contextFactory = contextFactory;
@ -29,7 +26,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{ {
await using var ctx = _contextFactory.CreateContext(enableTracking: false); using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId; int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases var iqAliasUpdates = ctx.Aliases

View File

@ -1,8 +1,11 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using ILogger = Microsoft.Extensions.Logging.ILogger; using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.Migration namespace IW4MAdmin.Application.Migration
{ {
@ -53,6 +56,7 @@ namespace IW4MAdmin.Application.Migration
if (!Directory.Exists(configDirectory)) if (!Directory.Exists(configDirectory))
{ {
log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory); Directory.CreateDirectory(configDirectory);
} }
@ -62,6 +66,7 @@ namespace IW4MAdmin.Application.Migration
foreach (var configFile in configurationFiles) foreach (var configFile in configurationFiles)
{ {
log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile); string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath)) if (!File.Exists(destinationPath))
{ {
@ -72,6 +77,7 @@ namespace IW4MAdmin.Application.Migration
if (!File.Exists(Path.Join("Database", "Database.db")) && if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db")) File.Exists("Database.db"))
{ {
log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db")); File.Move("Database.db", Path.Join("Database", "Database.db"));
} }
} }
@ -85,20 +91,5 @@ namespace IW4MAdmin.Application.Migration
config.ManualLogPath = null; config.ManualLogPath = null;
} }
} }
public static void RemoveObsoletePlugins20210322()
{
var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll", "IW4ScriptCommands.dll"};
foreach (var file in files)
{
var path = Path.Join(Utilities.OperatingDirectory, "Plugins", file);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
} }
} }

View File

@ -1,23 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client.Stats;
namespace IW4MAdmin.Application.Migration
{
public static class DatabaseHousekeeping
{
private static readonly DateTime CutoffDate = DateTime.UtcNow.AddMonths(-6);
public static async Task RemoveOldRatings(IDatabaseContextFactory contextFactory, CancellationToken token)
{
await using var context = contextFactory.CreateContext();
var dbSet = context.Set<EFRating>();
var itemsToDelete = dbSet.Where(rating => rating.When <= CutoffDate);
dbSet.RemoveRange(itemsToDelete);
await context.SaveChangesAsync(token);
}
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.Threading;
namespace IW4MAdmin.Application.Misc;
public class AsyncResult : IAsyncResult
{
public object AsyncState { get; set; }
public WaitHandle AsyncWaitHandle { get; set; }
public bool CompletedSynchronously { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -1,13 +1,10 @@
using SharedLibraryCore; using Newtonsoft.Json;
using SharedLibraryCore;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.IO; using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -17,40 +14,22 @@ namespace IW4MAdmin.Application.Misc
/// <typeparam name="T">base configuration type</typeparam> /// <typeparam name="T">base configuration type</typeparam>
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
{ {
private T _configuration; T _configuration;
private readonly SemaphoreSlim _onSaving;
private readonly JsonSerializerOptions _serializerOptions;
public BaseConfigurationHandler(string fn)
public BaseConfigurationHandler(string fileName)
{ {
_serializerOptions = new JsonSerializerOptions FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fn}.json");
{ Build();
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
_onSaving = new SemaphoreSlim(1, 1);
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fileName}.json");
}
public BaseConfigurationHandler() : this(typeof(T).Name)
{
}
~BaseConfigurationHandler()
{
_onSaving.Dispose();
} }
public string FileName { get; } public string FileName { get; }
public async Task BuildAsync() public void Build()
{ {
try try
{ {
await using var fileStream = File.OpenRead(FileName); var configContent = File.ReadAllText(FileName);
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions); _configuration = JsonConvert.DeserializeObject<T>(configContent);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
@ -60,7 +39,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e) catch (Exception e)
{ {
throw new ConfigurationException("Could not load configuration") throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
{ {
Errors = new[] { e.Message }, Errors = new[] { e.Message },
ConfigurationFileName = FileName ConfigurationFileName = FileName
@ -68,23 +47,16 @@ namespace IW4MAdmin.Application.Misc
} }
} }
public async Task Save() public Task Save()
{ {
try var settings = new JsonSerializerSettings()
{ {
await _onSaving.WaitAsync(); Formatting = Formatting.Indented
};
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
await using var fileStream = File.Create(FileName); var appConfigJSON = JsonConvert.SerializeObject(_configuration, settings);
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions); return File.WriteAllTextAsync(FileName, appConfigJSON);
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
} }
public T Configuration() public T Configuration()

View File

@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Data.Models;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IClientNoticeMessageFormatter
/// </summary>
public class ClientNoticeMessageFormatter : IClientNoticeMessageFormatter
{
private readonly ITranslationLookup _transLookup;
private readonly ApplicationConfiguration _appConfig;
public ClientNoticeMessageFormatter(ITranslationLookup transLookup, ApplicationConfiguration appConfig)
{
_transLookup = transLookup;
_appConfig = appConfig;
}
public string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null)
{
var isNewLineSeparator = config.NoticeLineSeparator == Environment.NewLine;
var penalty = originalPenalty ?? currentPenalty;
var builder = new StringBuilder();
// build the top level header
var header = _transLookup[$"SERVER_{penalty.Type.ToString().ToUpper()}_TEXT"];
builder.Append(header);
builder.Append(config.NoticeLineSeparator);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(reason);
builder.Append(config.NoticeLineSeparator);
}
if (penalty.Type == EFPenalty.PenaltyType.TempBan)
{
// build the time remaining if temporary
var timeRemainingValue = penalty.Expires.HasValue
? (penalty.Expires - DateTime.UtcNow).Value.HumanizeForCurrentCulture()
: "--";
var timeRemaining = _transLookup["GAME_MESSAGE_PENALTY_TIME_REMAINING"].FormatExt(timeRemainingValue);
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(timeRemaining);
}
}
if (penalty.Type == EFPenalty.PenaltyType.Ban)
{
// provide a place to appeal the ban (should always be specified but including a placeholder just incase)
builder.Append(_transLookup["GAME_MESSAGE_PENALTY_APPEAL"].FormatExt(_appConfig.ContactUri ?? "--"));
}
// final format looks something like:
/*
* You are permanently banned
* Reason - toxic behavior
* Visit example.com to appeal
*/
return builder.ToString();
}
private static IEnumerable<string> SplitOverMaxLength(string source, int maxCharactersPerLine)
{
if (source.Length <= maxCharactersPerLine)
{
return new[] {source};
}
var segments = new List<string>();
var currentLocation = 0;
while (currentLocation < source.Length)
{
var nextLocation = currentLocation + maxCharactersPerLine;
// there's probably a more efficient way to do this but this is readable
segments.Add(string.Concat(
source
.Skip(currentLocation)
.Take(Math.Min(maxCharactersPerLine, source.Length - currentLocation))));
currentLocation = nextLocation;
}
if (currentLocation < source.Length)
{
segments.Add(source.Substring(currentLocation, source.Length - currentLocation));
}
return segments;
}
}
}

View File

@ -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<TKey> : IComparer<TKey> 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<long, EventPerformance> Events { get; private set; } = new SortedList<long, EventPerformance>(new DuplicateKeyComparer<long>());
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}");
}
}
}

View File

@ -1,44 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
public class EventPublisher : IEventPublisher
{
public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect;
private readonly ILogger _logger;
public EventPublisher(ILogger<EventPublisher> logger)
{
_logger = logger;
}
public void Publish(GameEvent gameEvent)
{
_logger.LogDebug("Handling publishing event of type {EventType}", gameEvent.Type);
try
{
if (gameEvent.Type == GameEvent.EventType.Connect)
{
OnClientConnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{
OnClientDisconnect?.Invoke(this, gameEvent);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not publish event of type {EventType}", gameEvent.Type);
}
}
}
}

View File

@ -1,13 +0,0 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc;
public class GeoLocationResult : IGeoLocationResult
{
public string Country { get; set; }
public string CountryCode { get; set; }
public string Region { get; set; }
public string ASN { get; set; }
public string Timezone { get; set; }
public string Organization { get; set; }
}

View File

@ -1,40 +0,0 @@
using System;
using System.Threading.Tasks;
using MaxMind.GeoIP2;
using MaxMind.GeoIP2.Responses;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc;
public class GeoLocationService : IGeoLocationService
{
private readonly string _sourceAddress;
public GeoLocationService(string sourceAddress)
{
_sourceAddress = sourceAddress;
}
public Task<IGeoLocationResult> Locate(string address)
{
CountryResponse country = null;
try
{
using var reader = new DatabaseReader(_sourceAddress);
reader.TryCountry(address, out country);
}
catch
{
// ignored
}
var response = new GeoLocationResult
{
Country = country?.Country.Name ?? "Unknown",
CountryCode = country?.Country.IsoCode ?? ""
};
return Task.FromResult((IGeoLocationResult)response);
}
}

View File

@ -19,12 +19,6 @@ namespace IW4MAdmin.Application.Misc
/// </summary> /// </summary>
public string BasePathDirectory { get; set; } = ""; public string BasePathDirectory { get; set; } = "";
/// <summary>
/// directory for local storage
/// <remarks>fs_homepath</remarks>
/// </summary>
public string HomePathDirectory { get; set; } = "";
/// <summary> /// <summary>
/// overide game directory /// overide game directory
/// <remarks>plugin driven</remarks> /// <remarks>plugin driven</remarks>
@ -47,11 +41,5 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows /// indicates if running on windows
/// </summary> /// </summary>
public bool IsWindows { get; set; } = true; public bool IsWindows { get; set; } = true;
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
public bool IsOneLog { get; set; }
} }
} }

View File

@ -1,47 +1,132 @@
using System; using IW4MAdmin.Application.IO;
using Microsoft.Extensions.Logging; using SharedLibraryCore;
using ILogger = SharedLibraryCore.Interfaces.ILogger; using SharedLibraryCore.Interfaces;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
[Obsolete]
public class Logger : ILogger public class Logger : ILogger
{ {
private readonly Microsoft.Extensions.Logging.ILogger _logger; enum LogType
public Logger(ILogger<Logger> logger)
{ {
_logger = logger; Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
readonly string FileName;
readonly ReaderWriterLockSlim WritingLock;
static readonly short MAX_LOG_FILES = 10;
public Logger(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
WritingLock = new ReaderWriterLockSlim();
RotateLogs();
}
~Logger()
{
WritingLock.Dispose();
}
/// <summary>
/// rotates logs when log is initialized
/// </summary>
private void RotateLogs()
{
string maxLog = FileName + MAX_LOG_FILES;
if (File.Exists(maxLog))
{
File.Delete(maxLog);
}
for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
{
string logToMove = i == 0 ? FileName : FileName + i;
string movedLogName = FileName + (i + 1);
if (File.Exists(logToMove))
{
File.Move(logToMove, movedLogName);
}
}
}
void Write(string msg, LogType type)
{
WritingLock.EnterWriteLock();
string stringType = type.ToString();
msg = msg.StripColors();
try
{
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
try
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(msg);
#else
if (type == LogType.Error || type == LogType.Verbose)
{
Console.WriteLine(LogLine);
}
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
catch (Exception ex)
{
Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
Console.WriteLine(ex.GetExceptionInfo());
}
WritingLock.ExitWriteLock();
} }
public void WriteVerbose(string msg) public void WriteVerbose(string msg)
{ {
_logger.LogInformation(msg); Write(msg, LogType.Verbose);
} }
public void WriteDebug(string msg) public void WriteDebug(string msg)
{ {
_logger.LogDebug(msg); Write(msg, LogType.Debug);
} }
public void WriteError(string msg) public void WriteError(string msg)
{ {
_logger.LogError(msg); Write(msg, LogType.Error);
} }
public void WriteInfo(string msg) public void WriteInfo(string msg)
{ {
WriteVerbose(msg); Write(msg, LogType.Info);
} }
public void WriteWarning(string msg) public void WriteWarning(string msg)
{ {
_logger.LogWarning(msg); Write(msg, LogType.Warning);
} }
public void WriteAssert(bool condition, string msg) public void WriteAssert(bool condition, string msg)
{ {
throw new NotImplementedException(); if (!condition)
Write(msg, LogType.Assert);
} }
} }
} }

View File

@ -8,8 +8,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -26,10 +24,10 @@ namespace IW4MAdmin.Application.Misc
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99"); private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1; private readonly int _apiVersion = 1;
private bool _firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager) private bool firstHeartBeat = true;
public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{ {
_logger = logger; _logger = logger;
_transLookup = translationLookup; _transLookup = translationLookup;
@ -57,7 +55,13 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e) catch (Exception e)
{ {
_logger.LogWarning(e, "Unable to retrieve IW4MAdmin version information"); _logger.WriteWarning(_transLookup["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
_logger.WriteDebug(e.Message);
} }
if (version.CurrentVersionStable == _fallbackVersion) if (version.CurrentVersionStable == _fallbackVersion)
@ -94,24 +98,53 @@ namespace IW4MAdmin.Application.Misc
public async Task RunUploadStatus(CancellationToken token) public async Task RunUploadStatus(CancellationToken token)
{ {
// todo: clean up this logic
bool connected;
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
try try
{ {
if (_manager.IsRunning) await UploadStatus();
}
catch (System.Net.Http.HttpRequestException e)
{
_logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
catch (AggregateException e)
{
_logger.WriteWarning($"Could not send heartbeat - {e.Message}");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(ApiException));
foreach (var ex in exceptions)
{ {
await UploadStatus(); if (((ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
} }
} }
catch (Exception ex) catch (ApiException e)
{ {
_logger.LogWarning(ex, "Could not send heartbeat"); _logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
} }
catch (Exception e)
{
_logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
try try
{ {
await Task.Delay(Interval, token); await Task.Delay(30000, token);
} }
catch catch
@ -123,7 +156,7 @@ namespace IW4MAdmin.Application.Misc
private async Task UploadStatus() private async Task UploadStatus()
{ {
if (_firstHeartBeat) if (firstHeartBeat)
{ {
var token = await _apiInstance.Authenticate(new AuthenticationId var token = await _apiInstance.Authenticate(new AuthenticationId
{ {
@ -151,13 +184,12 @@ namespace IW4MAdmin.Application.Misc
Id = s.EndPoint, Id = s.EndPoint,
Port = (short)s.Port, Port = (short)s.Port,
IPAddress = s.IP IPAddress = s.IP
}).ToList(), }).ToList()
WebfrontUrl = _appConfig.WebfrontUrl
}; };
Response<ResultMessage> response = null; Response<ResultMessage> response = null;
if (_firstHeartBeat) if (firstHeartBeat)
{ {
response = await _apiInstance.AddInstance(instance); response = await _apiInstance.AddInstance(instance);
} }
@ -165,12 +197,12 @@ namespace IW4MAdmin.Application.Misc
else else
{ {
response = await _apiInstance.UpdateInstance(instance.Id, instance); response = await _apiInstance.UpdateInstance(instance.Id, instance);
_firstHeartBeat = false; firstHeartBeat = false;
} }
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK) if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{ {
_logger.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent); _logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}");
} }
} }
} }

View File

@ -7,10 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Models;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -18,21 +14,20 @@ namespace IW4MAdmin.Application.Misc
/// implementation of IMetaService /// implementation of IMetaService
/// used to add and retrieve runtime and persistent meta /// used to add and retrieve runtime and persistent meta
/// </summary> /// </summary>
[Obsolete("Use MetaServiceV2")]
public class MetaService : IMetaService public class MetaService : IMetaService
{ {
private readonly IDictionary<MetaType, List<dynamic>> _metaActions; private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
public MetaService(ILogger<MetaService> logger, IDatabaseContextFactory contextFactory) public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>(); _metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client, EFMeta linkedMeta = null) public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
{ {
// this seems to happen if the client disconnects before they've had time to authenticate and be added // this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1) if (client.ClientId < 1)
@ -40,7 +35,7 @@ namespace IW4MAdmin.Application.Misc
return; return;
} }
await using var ctx = _contextFactory.CreateContext(); using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -51,7 +46,6 @@ namespace IW4MAdmin.Application.Misc
{ {
existingMeta.Value = metaValue; existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow; existingMeta.Updated = DateTime.UtcNow;
existingMeta.LinkedMetaId = linkedMeta?.MetaId;
} }
else else
@ -61,124 +55,16 @@ namespace IW4MAdmin.Application.Misc
ClientId = client.ClientId, ClientId = client.ClientId,
Created = DateTime.UtcNow, Created = DateTime.UtcNow,
Key = metaKey, Key = metaKey,
Value = metaValue, Value = metaValue
LinkedMetaId = linkedMeta?.MetaId
}); });
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId)
{
await AddPersistentMeta(metaKey, metaValue, new EFClient { ClientId = clientId });
}
public async Task IncrementPersistentMeta(string metaKey, int incrementAmount, int clientId)
{
var existingMeta = await GetPersistentMeta(metaKey, new EFClient { ClientId = clientId });
if (!long.TryParse(existingMeta?.Value ?? "0", out var existingValue))
{
existingValue = 0;
}
var newValue = existingValue + incrementAmount;
await SetPersistentMeta(metaKey, newValue.ToString(), clientId);
}
public async Task DecrementPersistentMeta(string metaKey, int decrementAmount, int clientId)
{
await IncrementPersistentMeta(metaKey, -decrementAmount, clientId);
}
public async Task AddPersistentMeta(string metaKey, string metaValue)
{
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
var matchValues = existingMeta
.Where(meta => meta.Value == metaValue)
.ToArray();
if (matchValues.Any())
{
foreach (var meta in matchValues)
{
_logger.LogDebug("Updating existing meta with key {key} and id {id}", meta.Key, meta.MetaId);
meta.Value = metaValue;
meta.Updated = DateTime.UtcNow;
}
await ctx.SaveChangesAsync();
}
else
{
_logger.LogDebug("Adding new meta with key {key}", metaKey);
ctx.EFMeta.Add(new EFMeta()
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync();
}
}
public async Task RemovePersistentMeta(string metaKey, EFClient client)
{
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == client.ClientId);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {key} found for client id {id}", metaKey, client.ClientId);
return;
}
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync();
}
public async Task RemovePersistentMeta(string metaKey, string metaValue = null)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
if (metaValue == null)
{
_logger.LogDebug("Removing all meta for key {key} with ids [{ids}] ", metaKey, string.Join(", ", existingMeta.Select(meta => meta.MetaId)));
existingMeta.ForEach(meta => context.Remove(existingMeta));
await context.SaveChangesAsync();
return;
}
var foundMeta = existingMeta.FirstOrDefault(meta => meta.Value == metaValue);
if (foundMeta != null)
{
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, foundMeta.MetaId);
context.Remove(foundMeta);
await context.SaveChangesAsync();
}
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client) public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{ {
await using var ctx = _contextFactory.CreateContext(enableTracking: false); using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -188,34 +74,11 @@ namespace IW4MAdmin.Application.Misc
MetaId = _meta.MetaId, MetaId = _meta.MetaId,
Key = _meta.Key, Key = _meta.Key,
ClientId = _meta.ClientId, ClientId = _meta.ClientId,
Value = _meta.Value, Value = _meta.Value
LinkedMetaId = _meta.LinkedMetaId,
LinkedMeta = _meta.LinkedMetaId != null ? new EFMeta()
{
MetaId = _meta.LinkedMeta.MetaId,
Key = _meta.LinkedMeta.Key,
Value = _meta.LinkedMeta.Value
} : null
}) })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<IEnumerable<EFMeta>> GetPersistentMeta(string metaKey)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.ToListAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{ {
if (!_metaActions.ContainsKey(metaKey)) if (!_metaActions.ContainsKey(metaKey))
@ -231,30 +94,42 @@ namespace IW4MAdmin.Application.Misc
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request) public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
{ {
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information) var meta = new List<IClientMeta>();
.Select(async kvp => await kvp.Value[0](request)));
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m) foreach (var (type, actions) in _metaActions)
.OrderByDescending(m => m.When) {
// information is not listed chronologically
if (type != MetaType.Information)
{
var metaItems = await actions[0](request);
meta.AddRange(metaItems);
}
}
return meta.OrderByDescending(_meta => _meta.When)
.Take(request.Count) .Take(request.Count)
.ToList(); .ToList();
} }
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{ {
IEnumerable<T> meta;
if (metaType == MetaType.Information) if (metaType == MetaType.Information)
{ {
var allMeta = new List<T>(); var allMeta = new List<T>();
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration => foreach (var individualMetaRegistration in _metaActions[metaType])
(IEnumerable<T>)await individualMetaRegistration(request))); {
allMeta.AddRange(await individualMetaRegistration(request));
allMeta.AddRange(completedMeta.SelectMany(meta => meta)); }
return ProcessInformationMeta(allMeta); return ProcessInformationMeta(allMeta);
} }
var meta = await _metaActions[metaType][0](request) as IEnumerable<T>; else
{
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
}
return meta; return meta;
} }

View File

@ -1,453 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc;
public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == clientId)
.FirstOrDefaultAsync(token);
if (existingMeta != null)
{
_logger.LogDebug("Updating existing meta with key {Key} and id {Id}", existingMeta.Key,
existingMeta.MetaId);
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
}
else
{
_logger.LogDebug("Adding new meta with key {Key}", metaKey);
context.EFMeta.Add(new EFMeta
{
ClientId = clientId,
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue,
});
}
await context.SaveChangesAsync(token);
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,
CancellationToken token = default) where T : class
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
string serializedValue;
try
{
serializedValue = JsonSerializer.Serialize(metaValue);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with key {Key}", metaKey);
return;
}
await SetPersistentMeta(metaKey, serializedValue, clientId, token);
}
public async Task SetPersistentMetaForLookupKey(string metaKey, string lookupKey, int lookupId, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var lookupMeta = await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == lookupKey, token);
if (lookupMeta is null)
{
_logger.LogWarning("No lookup meta exists for metaKey {MetaKey} and lookupKey {LookupKey}", metaKey,
lookupKey);
return;
}
var lookupValues = JsonSerializer.Deserialize<List<LookupValue<string>>>(lookupMeta.Value);
if (lookupValues is null)
{
return;
}
var foundLookup = lookupValues.FirstOrDefault(value => value.Id == lookupId);
if (foundLookup is null)
{
_logger.LogWarning("No lookup meta found for provided lookup id {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
return;
}
_logger.LogDebug("Setting meta for lookup {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
await SetPersistentMeta(metaKey, lookupId.ToString(), clientId, token);
}
public async Task IncrementPersistentMeta(string metaKey, int incrementAmount, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
var existingMeta = await GetPersistentMeta(metaKey, clientId, token);
if (!long.TryParse(existingMeta?.Value ?? "0", out var existingValue))
{
existingValue = 0;
}
var newValue = existingValue + incrementAmount;
await SetPersistentMeta(metaKey, newValue.ToString(), clientId, token);
}
public async Task DecrementPersistentMeta(string metaKey, int decrementAmount, int clientId,
CancellationToken token = default)
{
await IncrementPersistentMeta(metaKey, -decrementAmount, clientId, token);
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, int clientId, CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return null;
}
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == clientId)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.FirstOrDefaultAsync(token);
}
public async Task<T> GetPersistentMetaValue<T>(string metaKey, int clientId, CancellationToken token = default)
where T : class
{
var meta = await GetPersistentMeta(metaKey, clientId, token);
if (meta is null)
{
return default;
}
try
{
return JsonSerializer.Deserialize<T>(meta.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not deserialize meta with key {Key} and value {Value}", metaKey, meta.Value);
return default;
}
}
public async Task<EFMeta> GetPersistentMetaByLookup(string metaKey, string lookupKey, int clientId,
CancellationToken token = default)
{
await using var context = _contextFactory.CreateContext();
var metaValue = await GetPersistentMeta(metaKey, clientId, token);
if (metaValue is null)
{
_logger.LogDebug("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId);
return default;
}
var lookupMeta = await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == lookupKey, token);
if (lookupMeta is null)
{
_logger.LogWarning("No lookup meta exists for metaKey {MetaKey} and lookupKey {LookupKey}", metaKey,
lookupKey);
return default;
}
var lookupId = int.Parse(metaValue.Value);
var lookupValues = JsonSerializer.Deserialize<List<LookupValue<string>>>(lookupMeta.Value);
if (lookupValues is null)
{
return default;
}
var foundLookup = lookupValues.FirstOrDefault(value => value.Id == lookupId);
if (foundLookup is not null)
{
return new EFMeta
{
Created = metaValue.Created,
Updated = metaValue.Updated,
Extra = metaValue.Extra,
MetaId = metaValue.MetaId,
Value = foundLookup.Value
};
}
_logger.LogWarning("No lookup meta found for provided lookup id {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
return default;
}
public async Task RemovePersistentMeta(string metaKey, int clientId, CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == clientId, token);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {Key} found for client id {Id}", metaKey, clientId);
return;
}
_logger.LogDebug("Removing meta for key {Key} with id {Id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync(token);
}
public async Task SetPersistentMeta(string metaKey, string metaValue, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
_logger.LogWarning("Cannot save meta with no key");
return;
}
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.FirstOrDefaultAsync(token);
if (existingMeta is not null)
{
_logger.LogDebug("Updating existing meta with key {Key} and id {Id}", existingMeta.Key,
existingMeta.MetaId);
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
await ctx.SaveChangesAsync(token);
}
else
{
_logger.LogDebug("Adding new meta with key {Key}", metaKey);
ctx.EFMeta.Add(new EFMeta
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync(token);
}
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, CancellationToken token = default)
where T : class
{
if (string.IsNullOrWhiteSpace(metaKey))
{
_logger.LogWarning("Meta key is null, not setting");
return;
}
if (metaValue is null)
{
_logger.LogWarning("Meta value is null, not setting");
return;
}
string serializedMeta;
try
{
serializedMeta = JsonSerializer.Serialize(metaValue);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with {Key} and value {Value}", metaKey, metaValue);
return;
}
await SetPersistentMeta(metaKey, serializedMeta, token);
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return null;
}
await using var context = _contextFactory.CreateContext(false);
return await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == metaKey, token);
}
public async Task<T> GetPersistentMetaValue<T>(string metaKey, CancellationToken token = default) where T : class
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return default;
}
var meta = await GetPersistentMeta(metaKey, token);
if (meta is null)
{
return default;
}
try
{
return JsonSerializer.Deserialize<T>(meta.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with key {Key} and value {Value}", metaKey, meta.Value);
return default;
}
}
public async Task RemovePersistentMeta(string metaKey, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.FirstOrDefaultAsync(token);
if (existingMeta != null)
{
_logger.LogDebug("Removing meta for key {Key} with id {Id}", metaKey, existingMeta.MetaId);
context.Remove(existingMeta);
await context.SaveChangesAsync(token);
}
}
public void AddRuntimeMeta<T, TReturnType>(MetaType metaKey,
Func<T, CancellationToken, Task<IEnumerable<TReturnType>>> metaAction)
where T : PaginationRequest where TReturnType : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
{
_metaActions.Add(metaKey, new List<dynamic> { metaAction });
}
else
{
_metaActions[metaKey].Add(metaAction);
}
}
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request, CancellationToken token = default)
{
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information)
.Select(async kvp => await kvp.Value[0](request, token)));
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
.OrderByDescending(m => m.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType, CancellationToken token = default)
where T : IClientMeta
{
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
(IEnumerable<T>)await individualMetaRegistration(request, token)));
allMeta.AddRange(completedMeta.SelectMany(meta => meta));
return ProcessInformationMeta(allMeta);
}
var meta = await _metaActions[metaType][0](request, token) as IEnumerable<T>;
return meta;
}
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{
return meta;
}
private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0;
}

View File

@ -1,9 +1,8 @@
using SharedLibraryCore.Interfaces; using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -12,7 +11,7 @@ namespace IW4MAdmin.Application.Misc
private readonly IDictionary<string, IList<object>> _actions; private readonly IDictionary<string, IList<object>> _actions;
private readonly ILogger _logger; private readonly ILogger _logger;
public MiddlewareActionHandler(ILogger<MiddlewareActionHandler> logger) public MiddlewareActionHandler(ILogger logger)
{ {
_actions = new Dictionary<string, IList<object>>(); _actions = new Dictionary<string, IList<object>>();
_logger = logger; _logger = logger;
@ -39,7 +38,8 @@ namespace IW4MAdmin.Application.Misc
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogWarning(e, "Failed to invoke middleware action {name}", name); _logger.WriteWarning($"Failed to invoke middleware action {name}");
_logger.WriteDebug(e.GetExceptionInfo());
} }
} }

View File

@ -5,13 +5,9 @@ using System.Reflection;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using SharedLibraryCore; using SharedLibraryCore;
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Helpers
{ {
/// <summary> /// <summary>
/// implementation of IPluginImporter /// implementation of IPluginImporter
@ -19,19 +15,12 @@ namespace IW4MAdmin.Application.Misc
/// </summary> /// </summary>
public class PluginImporter : IPluginImporter public class PluginImporter : IPluginImporter
{ {
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private static readonly string PLUGIN_DIR = "Plugins"; private static readonly string PLUGIN_DIR = "Plugins";
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler) public PluginImporter(ILogger logger)
{ {
_logger = logger; _logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
} }
/// <summary> /// <summary>
@ -40,138 +29,60 @@ namespace IW4MAdmin.Application.Misc
/// <returns></returns> /// <returns></returns>
public IEnumerable<IPlugin> DiscoverScriptPlugins() public IEnumerable<IPlugin> DiscoverScriptPlugins()
{ {
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}"; string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
if (!Directory.Exists(pluginDir)) if (Directory.Exists(pluginDir))
{ {
return Enumerable.Empty<IPlugin>(); string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js");
_logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins");
if (scriptPluginFiles.Length > 0)
{
foreach (string fileName in scriptPluginFiles)
{
_logger.WriteInfo($"Discovered script plugin {fileName}");
var plugin = new ScriptPlugin(fileName);
yield return plugin;
}
}
} }
var scriptPluginFiles =
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count);
return scriptPluginFiles.Select(fileName =>
{
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
return new ScriptPlugin(_logger, fileName);
}).ToList();
} }
/// <summary> /// <summary>
/// discovers all the C# assembly plugins and commands /// discovers all the C# assembly plugins and commands
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations() public (IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{ {
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}"; string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty<Type>(); var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>(); var commandTypes = Enumerable.Empty<Type>();
var configurationTypes = Enumerable.Empty<Type>();
if (Directory.Exists(pluginDir)) if (Directory.Exists(pluginDir))
{ {
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll"); var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length); _logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies");
if (dllFileNames.Length > 0) if (dllFileNames.Length > 0)
{ {
// we only want to load the most recent assembly in case of duplicates var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name));
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name))
.Union(GetRemoteAssemblies())
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies pluginTypes = assemblies
.SelectMany(_asm => .SelectMany(_asm => _asm.GetTypes())
{
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null); .Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count()); _logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations");
commandTypes = assemblies commandTypes = assemblies
.SelectMany(_asm =>{ .SelectMany(_asm => _asm.GetTypes())
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command)); .Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count()); _logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands");
configurationTypes = assemblies
.SelectMany(asm => {
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(asmType =>
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
_logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
} }
} }
return (pluginTypes, commandTypes, configurationTypes); return (pluginTypes, commandTypes);
} }
private IEnumerable<Assembly> GetRemoteAssemblies()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load remote assemblies");
return Enumerable.Empty<Assembly>();
}
}
private IEnumerable<string> GetRemoteScripts()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex,"Could not load remote scripts");
return Enumerable.Empty<string>();
}
}
}
public enum PluginType
{
Binary,
Script
} }
} }

View File

@ -1,76 +0,0 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
public class RemoteAssemblyHandler : IRemoteAssemblyHandler
{
private const int keyLength = 32;
private const int tagLength = 16;
private const int nonceLength = 12;
private const int iterationCount = 10000;
private readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger;
public RemoteAssemblyHandler(ILogger<RemoteAssemblyHandler> logger, ApplicationConfiguration appconfig)
{
_appconfig = appconfig;
_logger = logger;
}
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
{
return DecryptContent(encryptedAssemblies)
.Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
}
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
{
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
}
private byte[][] DecryptContent(string[] content)
{
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
{
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return new byte[0][];
}
var assemblies = content.Select(piece =>
{
byte[] byteContent = Convert.FromBase64String(piece);
byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
byte[] decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(keyLength));
try
{
encryption.Decrypt(nonce, encryptedContent, tag, decryptedContent);
}
catch (CryptographicException ex)
{
_logger.LogError(ex, "Could not decrypt remote plugin assemblies");
}
return decryptedContent;
});
return assemblies.ToArray();
}
}
}

View File

@ -4,9 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client; using static SharedLibraryCore.Database.Models.EFClient;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -15,41 +13,30 @@ namespace IW4MAdmin.Application.Misc
/// </summary> /// </summary>
public class ScriptCommand : Command public class ScriptCommand : Command
{ {
private readonly Func<GameEvent, Task> _executeAction; private readonly Action<GameEvent> _executeAction;
private readonly ILogger _logger;
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
EFClient.Permission permission, CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
: base(config, layout) : base(config, layout)
{ {
_executeAction = executeAction; _executeAction = executeAction;
_logger = logger;
Name = name; Name = name;
Alias = alias; Alias = alias;
Description = description; Description = description;
RequiresTarget = isTargetRequired; RequiresTarget = isTargetRequired;
Permission = permission; Permission = permission;
Arguments = args; Arguments = args;
SupportedGames = supportedGames;
} }
public override async Task ExecuteAsync(GameEvent e) public override Task ExecuteAsync(GameEvent E)
{ {
if (_executeAction == null) if (_executeAction == null)
{ {
throw new InvalidOperationException($"No execute action defined for command \"{Name}\""); throw new InvalidOperationException($"No execute action defined for command \"{Name}\"");
} }
try return Task.Run(() => _executeAction(E));
{
await _executeAction(e);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
}
} }
} }
} }

View File

@ -1,5 +1,4 @@
using System; using Jint;
using Jint;
using Jint.Native; using Jint.Native;
using Jint.Runtime; using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder; using Microsoft.CSharp.RuntimeBinder;
@ -13,10 +12,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -37,27 +32,26 @@ namespace IW4MAdmin.Application.Misc
/// </summary> /// </summary>
public bool IsParser { get; private set; } public bool IsParser { get; private set; }
public FileSystemWatcher Watcher { get; } public FileSystemWatcher Watcher { get; private set; }
private Engine _scriptEngine; private Engine _scriptEngine;
private readonly string _fileName; private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1); private readonly SemaphoreSlim _onProcessing;
private bool _successfullyLoaded; private bool successfullyLoaded;
private readonly List<string> _registeredCommandNames; private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null) public ScriptPlugin(string filename, string workingDirectory = null)
{ {
_logger = logger;
_fileName = filename; _fileName = filename;
Watcher = new FileSystemWatcher Watcher = new FileSystemWatcher()
{ {
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory,
NotifyFilter = NotifyFilters.Size, NotifyFilter = NotifyFilters.Size,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last() Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
}; };
Watcher.EnableRaisingEvents = true; Watcher.EnableRaisingEvents = true;
_onProcessing = new SemaphoreSlim(1, 1);
_registeredCommandNames = new List<string>(); _registeredCommandNames = new List<string>();
} }
@ -67,13 +61,12 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose(); _onProcessing.Dispose();
} }
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
IScriptPluginServiceResolver serviceResolver)
{ {
await _onProcessing.WaitAsync();
try try
{ {
await _onProcessing.WaitAsync();
// for some reason we get an event trigger when the file is not finished being modified. // for some reason we get an event trigger when the file is not finished being modified.
// this must have been a change in .NET CORE 3.x // this must have been a change in .NET CORE 3.x
// so if the new file is empty we can't process it yet // so if the new file is empty we can't process it yet
@ -82,27 +75,26 @@ namespace IW4MAdmin.Application.Misc
return; return;
} }
var firstRun = _scriptEngine == null; bool firstRun = _scriptEngine == null;
// it's been loaded before so we need to call the unload event // it's been loaded before so we need to call the unload event
if (!firstRun) if (!firstRun)
{ {
await OnUnloadAsync(); await OnUnloadAsync();
foreach (var commandName in _registeredCommandNames) foreach (string commandName in _registeredCommandNames)
{ {
_logger.LogDebug("Removing plugin registered command {Command}", commandName); manager.GetLogger(0).WriteDebug($"Removing plugin registered command \"{commandName}\"");
manager.RemoveCommandByName(commandName); manager.RemoveCommandByName(commandName);
} }
_registeredCommandNames.Clear(); _registeredCommandNames.Clear();
} }
_successfullyLoaded = false; successfullyLoaded = false;
string script; string script;
await using (var stream = using (var stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{ {
using (var reader = new StreamReader(stream, Encoding.Default)) using (var reader = new StreamReader(stream, Encoding.Default))
{ {
@ -112,34 +104,24 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine = new Engine(cfg => _scriptEngine = new Engine(cfg =>
cfg.AllowClr(new[] cfg.AllowClr(new[]
{ {
typeof(System.Net.Http.HttpClient).Assembly, typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly, typeof(EFClient).Assembly,
typeof(Utilities).Assembly, typeof(Utilities).Assembly,
typeof(Encoding).Assembly typeof(Encoding).Assembly
}) })
.CatchClrExceptions() .CatchClrExceptions());
.AddObjectConverter(new PermissionLevelToStringConverter()));
_scriptEngine.Execute(script); _scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver); _scriptEngine.SetValue("_serviceResolver", serviceResolver);
_scriptEngine.SetValue("_lock", _onProcessing); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
Author = pluginObject.author; Author = pluginObject.author;
Name = pluginObject.name; Name = pluginObject.name;
Version = (float)pluginObject.version; Version = (float)pluginObject.version;
var commands = JsValue.Undefined; var commands = _scriptEngine.GetValue("commands");
try
{
commands = _scriptEngine.Evaluate("commands");
}
catch (JavaScriptException)
{
// ignore because commands aren't defined;
}
if (commands != JsValue.Undefined) if (commands != JsValue.Undefined)
{ {
@ -147,7 +129,7 @@ namespace IW4MAdmin.Application.Misc
{ {
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory)) foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{ {
_logger.LogDebug("Adding plugin registered command {CommandName}", command.Name); manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\"");
manager.AddAdditionalCommand(command); manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name); _registeredCommandNames.Add(command.Name);
} }
@ -155,63 +137,44 @@ namespace IW4MAdmin.Application.Misc
catch (RuntimeBinderException e) catch (RuntimeBinderException e)
{ {
throw new PluginException($"Not all required fields were found: {e.Message}") throw new PluginException($"Not all required fields were found: {e.Message}") { PluginFile = _fileName };
{ PluginFile = _fileName };
} }
} }
await OnLoadAsync(manager);
try try
{ {
if (pluginObject.isParser) if (pluginObject.isParser)
{ {
await OnLoadAsync(manager);
IsParser = true; IsParser = true;
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject(); IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject(); IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser); manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser); manager.AdditionalRConParsers.Add(rconParser);
} }
} }
catch (RuntimeBinderException) catch (RuntimeBinderException) { }
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
await configWrapper.InitializeAsync();
_scriptEngine.SetValue("_configHandler", configWrapper);
await OnLoadAsync(manager);
}
if (!firstRun) if (!firstRun)
{ {
await OnLoadAsync(manager); await OnLoadAsync(manager);
} }
_successfullyLoaded = true; successfullyLoaded = true;
} }
catch (JavaScriptException ex) catch (JavaScriptException ex)
{ {
_logger.LogError(ex, throw new PluginException($"An error occured while initializing script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("An error occured while initializing script plugin");
} }
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
catch
{ {
_logger.LogError(ex, throw;
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo}",
nameof(Initialize), _fileName, jsEx.Location);
throw new PluginException("An error occured while initializing script plugin");
} }
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnLoadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onProcessing.CurrentCount == 0)
@ -221,120 +184,54 @@ namespace IW4MAdmin.Application.Misc
} }
} }
public async Task OnEventAsync(GameEvent gameEvent, Server server) public async Task OnEventAsync(GameEvent E, Server S)
{ {
if (!_successfullyLoaded) if (successfullyLoaded)
{
return;
}
try
{ {
await _onProcessing.WaitAsync(); await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_gameEvent", gameEvent);
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
_scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}
catch (JavaScriptException ex) try
{
using (LogContext.PushProperty("Server", server.ToString()))
{ {
_logger.LogError(ex, _scriptEngine.SetValue("_gameEvent", E);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {EventType} {@LocationInfo}", _scriptEngine.SetValue("_server", S);
nameof(OnEventAsync), Path.GetFileName(_fileName), gameEvent.Type, ex.Location); _scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
} }
throw new PluginException("An error occured while executing action for script plugin"); catch
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{ {
_logger.LogError(ex, throw;
"Encountered error while running {MethodName} for script plugin {Plugin} with event type {EventType}",
nameof(OnEventAsync), _fileName, gameEvent.Type);
} }
throw new PluginException("An error occured while executing action for script plugin"); finally
}
finally
{
if (_onProcessing.CurrentCount == 0)
{ {
_onProcessing.Release(1); if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
} }
} }
} }
public Task OnLoadAsync(IManager manager) public Task OnLoadAsync(IManager manager)
{ {
try manager.GetLogger(0).WriteDebug($"OnLoad executing for {Name}");
{ _scriptEngine.SetValue("_manager", manager);
_logger.LogDebug("OnLoad executing for {Name}", Name); return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnLoadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnLoadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
} }
public async Task OnTickAsync(Server server) public Task OnTickAsync(Server S)
{ {
_scriptEngine.SetValue("_server", server); _scriptEngine.SetValue("_server", S);
await Task.FromResult(_scriptEngine.Evaluate("plugin.onTickAsync(_server)")); return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
} }
public Task OnUnloadAsync() public async Task OnUnloadAsync()
{ {
if (!_successfullyLoaded) if (successfullyLoaded)
{ {
return Task.CompletedTask; await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
} }
try
{
_scriptEngine.Evaluate("plugin.onUnloadAsync()");
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
return Task.CompletedTask;
} }
/// <summary> /// <summary>
@ -343,10 +240,9 @@ namespace IW4MAdmin.Application.Misc
/// <param name="commands">commands value from jint parser</param> /// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param> /// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns> /// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, public IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
IScriptCommandFactory scriptCommandFactory)
{ {
var commandList = new List<IManagerCommand>(); List<IManagerCommand> commandList = new List<IManagerCommand>();
// go through each defined command // go through each defined command
foreach (var command in commands.AsArray()) foreach (var command in commands.AsArray())
@ -356,10 +252,9 @@ namespace IW4MAdmin.Application.Misc
string alias = dynamicCommand.alias; string alias = dynamicCommand.alias;
string description = dynamicCommand.description; string description = dynamicCommand.description;
string permission = dynamicCommand.permission; string permission = dynamicCommand.permission;
List<Server.Game> supportedGames = null; bool targetRequired = false;
var targetRequired = false;
var args = new List<(string, bool)>(); List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null; dynamic arguments = null;
try try
@ -390,152 +285,26 @@ namespace IW4MAdmin.Application.Misc
} }
} }
try void execute(GameEvent e)
{ {
foreach (var game in dynamicCommand.supportedGames) _scriptEngine.SetValue("_event", e);
{ var jsEventObject = _scriptEngine.GetValue("_event");
supportedGames ??= new List<Server.Game>();
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
}
}
catch (RuntimeBinderException)
{
// supported games is optional
}
async Task Execute(GameEvent gameEvent)
{
try try
{ {
await _onProcessing.WaitAsync(); dynamicCommand.execute.Target.Invoke(jsEventObject);
_scriptEngine.SetValue("_event", gameEvent);
var jsEventObject = _scriptEngine.Evaluate("_event");
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
} }
catch (JavaScriptException ex) catch (JavaScriptException ex)
{ {
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString())) throw new PluginException($"An error occured while executing action for script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
{
_logger.LogError(ex, "Could not execute command action for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
{
_logger.LogError(ex,
"Could not execute command action for script plugin {FileName}",
Path.GetFileName(_fileName));
}
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
} }
} }
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
targetRequired, args, Execute, supportedGames?.ToArray()));
} }
return commandList; return commandList;
} }
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
}, tokenSource.Token);
}
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
}, tokenSource.Token);
}
}
public class PermissionLevelToStringConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is Data.Models.Client.EFClient.Permission)
{
result = value.ToString();
return true;
}
result = JsValue.Null;
return false;
}
} }
} }

View File

@ -1,96 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using IW4MAdmin.Application.Configuration;
using Jint;
using Jint.Native;
using Newtonsoft.Json.Linq;
namespace IW4MAdmin.Application.Misc
{
public class ScriptPluginConfigurationWrapper
{
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
private ScriptPluginConfiguration _config;
private readonly string _pluginName;
private readonly Engine _scriptEngine;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
{
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
_pluginName = pluginName;
_scriptEngine = scriptEngine;
}
public async Task InitializeAsync()
{
await _handler.BuildAsync();
_config = _handler.Configuration() ??
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
}
private static int? AsInteger(double d)
{
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double d)
{
castValue = AsInteger(d) ?? value;
}
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
{
castValue = array.Select(item => AsInteger((double)item)).ToArray();
}
if (!_config.ContainsKey(_pluginName))
{
_config.Add(_pluginName, new Dictionary<string, object>());
}
var plugin = _config[_pluginName];
if (plugin.ContainsKey(key))
{
plugin[key] = castValue;
}
else
{
plugin.Add(key, castValue);
}
_handler.Set(_config);
await _handler.Save();
}
public JsValue GetValue(string key)
{
if (!_config.ContainsKey(_pluginName))
{
return JsValue.Undefined;
}
if (!_config[_pluginName].ContainsKey(key))
{
return JsValue.Undefined;
}
var item = _config[_pluginName][key];
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
{
item = jElem.Deserialize<List<dynamic>>();
}
return JsValue.FromObject(_scriptEngine, item);
}
}
}

View File

@ -1,159 +0,0 @@
using System;
using System.Threading;
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc;
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
private Timer _timer;
private Action _actions;
private Delegate _jsAction;
private string _actionName;
private const int DefaultDelay = 0;
private const int DefaultInterval = 1000;
private readonly ILogger _logger;
private readonly ManualResetEventSlim _onRunningTick = new();
private SemaphoreSlim _onDependentAction;
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
{
_logger = logger;
}
~ScriptPluginTimerHelper()
{
if (_timer != null)
{
Stop();
}
_onRunningTick.Dispose();
}
public void Start(int delay, int interval)
{
if (_actions is null)
{
throw new InvalidOperationException("Timer action must be defined before starting");
}
if (delay < 0)
{
throw new ArgumentException("Timer delay must be >= 0");
}
if (interval < 20)
{
throw new ArgumentException("Timer interval must be at least 20ms");
}
Stop();
_logger.LogDebug("Starting script timer...");
_onRunningTick.Set();
_timer ??= new Timer(callback => _actions(), null, delay, interval);
IsRunning = true;
}
public void Start(int interval)
{
Start(DefaultDelay, interval);
}
public void Start()
{
Start(DefaultDelay, DefaultInterval);
}
public void Stop()
{
if (_timer == null)
{
return;
}
_logger.LogDebug("Stopping script timer...");
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
_timer = null;
IsRunning = false;
}
public void OnTick(Delegate action, string actionName)
{
if (string.IsNullOrEmpty(actionName))
{
throw new ArgumentException("actionName must be provided", nameof(actionName));
}
if (action is null)
{
throw new ArgumentException("action must be provided", nameof(action));
}
_logger.LogDebug("Adding new action with name {ActionName}", actionName);
_jsAction = action;
_actionName = actionName;
_actions = OnTick;
}
private void ReleaseThreads()
{
_onRunningTick.Set();
if (_onDependentAction?.CurrentCount != 0)
{
return;
}
_onDependentAction?.Release(1);
}
private void OnTick()
{
try
{
if (!_onRunningTick.IsSet)
{
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
nameof(OnTick));
return;
}
_onRunningTick.Reset();
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
_onDependentAction?.WaitAsync().Wait();
var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
ReleaseThreads();
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsex)
{
_logger.LogError(jsex,
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
jsex.Location);
ReleaseThreads();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
_onRunningTick.Set();
ReleaseThreads();
}
}
public void SetDependency(SemaphoreSlim dependentSemaphore)
{
_onDependentAction = dependentSemaphore;
}
public bool IsRunning { get; private set; }
}

View File

@ -4,7 +4,6 @@ using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using System; using System;
using System.Net; using System.Net;
using Data.Models;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;

View File

@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats.Reference;
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;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataCollector : IServerDataCollector
{
private readonly ILogger _logger;
private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
private readonly IEventPublisher _eventPublisher;
private bool _inProgress;
private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
_eventPublisher = eventPublisher;
_eventPublisher.OnClientConnect += SaveConnectionInfo;
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
}
~ServerDataCollector()
{
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
{
if (_inProgress)
{
throw new InvalidOperationException($"{nameof(ServerDataCollector)} is already collecting data");
}
_logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector));
_inProgress = true;
_period = period ?? (Utilities.IsDevelopment
? TimeSpan.FromMinutes(1)
: _appConfig.ServerDataCollectionInterval);
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(_period, cancellationToken);
_logger.LogDebug("{Name} is collecting server data", nameof(ServerDataCollector));
var data = await BuildCollectionData(cancellationToken);
await SaveData(data, cancellationToken);
}
catch (TaskCanceledException)
{
_logger.LogInformation("Shutdown requested for {Name}", nameof(ServerDataCollector));
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error encountered collecting server data for {Name}",
nameof(ServerDataCollector));
}
}
}
private async Task<IEnumerable<EFServerSnapshot>> BuildCollectionData(CancellationToken token)
{
var data = await Task.WhenAll(_manager.GetServers()
.Select(async server => new EFServerSnapshot
{
CapturedAt = DateTime.UtcNow,
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
ServerId = await server.GetIdForServer(),
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
ClientCount = server.ClientNum,
ConnectionInterrupted = server.Throttled,
}));
return data;
}
private async Task<int> GetOrCreateMap(string mapName, Reference.Game game, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
var existingMap =
await context.Maps.FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token);
if (existingMap != null)
{
return existingMap.MapId;
}
var newMap = new EFMap
{
Name = mapName,
Game = game
};
context.Maps.Add(newMap);
await context.SaveChangesAsync(token);
return newMap.MapId;
}
private async Task SaveData(IEnumerable<EFServerSnapshot> snapshots, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
context.ServerSnapshots.AddRange(snapshots);
await context.SaveChangesAsync(token);
}
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
{
using var context = _contextFactory.CreateContext(enableTracking: false);
context.ConnectionHistory.Add(new EFClientConnectionHistory
{
ClientId = gameEvent.Origin.ClientId,
ServerId = gameEvent.Owner.GetIdForServer().Result,
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
context.SaveChanges();
}
}
}

View File

@ -1,192 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataViewer : IServerDataViewer
{
private readonly ILogger _logger;
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
}
public async Task<(int?, DateTime?)>
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
CancellationToken token = default)
{
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
{
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1);
int? maxClients;
DateTime? maxClientsTime;
if (serverId != null)
{
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.OrderByDescending(snapshot => snapshot.ClientCount)
.Select(snapshot => new
{
snapshot.ClientCount,
snapshot.CapturedAt
})
.FirstOrDefaultAsync(cancellationToken);
maxClients = clients?.ClientCount;
maxClientsTime = clients?.CapturedAt;
}
else
{
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => new
{
ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount),
Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt)
})
.OrderByDescending(snapshot => snapshot.ClientCount)
.FirstOrDefaultAsync(cancellationToken);
maxClients = clients?.ClientCount;
maxClientsTime = clients?.Time;
}
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return (maxClients, maxClientsTime);
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
try
{
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(MaxConcurrentClientsAsync));
return (null, null);
}
}
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
{
var count = await set.CountAsync(cancellationToken);
var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
cancellationToken);
return (count, recentCount);
}, nameof(_serverStatsCache), _cacheTimeSpan, true);
try
{
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientCountsAsync));
return (0, 0);
}
}
public async Task<IEnumerable<ClientHistoryInfo>> 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,
snapshot.ConnectionInterrupted,
MapName = snapshot.Map.Name,
})
.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, ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName}).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<ClientHistoryInfo>();
}
}
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
}
}

View File

@ -7,43 +7,42 @@ using System.Text;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
internal class TokenAuthentication : ITokenAuthentication class TokenAuthentication : ITokenAuthentication
{ {
private readonly ConcurrentDictionary<int, TokenState> _tokens; private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly RandomNumberGenerator _random; private readonly RNGCryptoServiceProvider _random;
private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120); private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120);
private const short TokenLength = 4; private const short TOKEN_LENGTH = 4;
public TokenAuthentication() public TokenAuthentication()
{ {
_tokens = new ConcurrentDictionary<int, TokenState>(); _tokens = new ConcurrentDictionary<long, TokenState>();
_random = RandomNumberGenerator.Create(); _random = new RNGCryptoServiceProvider();
} }
public bool AuthorizeToken(ITokenIdentifier authInfo) public bool AuthorizeToken(long networkId, string token)
{ {
var authorizeSuccessful = _tokens.ContainsKey(authInfo.ClientId) && bool authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
_tokens[authInfo.ClientId].Token == authInfo.Token;
if (authorizeSuccessful) if (authorizeSuccessful)
{ {
_tokens.TryRemove(authInfo.ClientId, out _); _tokens.TryRemove(networkId, out TokenState _);
} }
return authorizeSuccessful; return authorizeSuccessful;
} }
public TokenState GenerateNextToken(ITokenIdentifier authInfo) public TokenState GenerateNextToken(long networkId)
{ {
TokenState state; TokenState state = null;
if (_tokens.ContainsKey(authInfo.ClientId)) if (_tokens.ContainsKey(networkId))
{ {
state = _tokens[authInfo.ClientId]; state = _tokens[networkId];
if (DateTime.Now - state.RequestTime > TimeoutPeriod) if ((DateTime.Now - state.RequestTime) > _timeoutPeriod)
{ {
_tokens.TryRemove(authInfo.ClientId, out _); _tokens.TryRemove(networkId, out TokenState _);
} }
else else
@ -52,42 +51,43 @@ namespace IW4MAdmin.Application.Misc
} }
} }
state = new TokenState state = new TokenState()
{ {
NetworkId = networkId,
Token = _generateToken(), Token = _generateToken(),
TokenDuration = TimeoutPeriod TokenDuration = _timeoutPeriod
}; };
_tokens.TryAdd(authInfo.ClientId, state); _tokens.TryAdd(networkId, state);
// perform some housekeeping so we don't have built up tokens if they're not ever used // perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens) foreach (var (key, value) in _tokens)
{ {
if (DateTime.Now - value.RequestTime > TimeoutPeriod) if ((DateTime.Now - value.RequestTime) > _timeoutPeriod)
{ {
_tokens.TryRemove(key, out _); _tokens.TryRemove(key, out TokenState _);
} }
} }
return state; return state;
} }
private string _generateToken() public string _generateToken()
{ {
bool ValidCharacter(char c) bool validCharacter(char c)
{ {
// this ensure that the characters are 0-9, A-Z, a-z // this ensure that the characters are 0-9, A-Z, a-z
return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123); return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123);
} }
var token = new StringBuilder(); StringBuilder token = new StringBuilder();
var charSet = new byte[1]; while (token.Length < TOKEN_LENGTH)
while (token.Length < TokenLength)
{ {
byte[] charSet = new byte[1];
_random.GetBytes(charSet); _random.GetBytes(charSet);
if (ValidCharacter((char)charSet[0])) if (validCharacter((char)charSet[0]))
{ {
token.Append((char)charSet[0]); token.Append((char)charSet[0]);
} }

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
namespace Integrations.Cod namespace IW4MAdmin.Application.RCon
{ {
/// <summary> /// <summary>
/// used to keep track of the udp connection state /// used to keep track of the udp connection state
@ -18,21 +18,14 @@ namespace Integrations.Cod
} }
public int ConnectionAttempts { get; set; } public int ConnectionAttempts { get; set; }
private const int BufferSize = 16384; const int BufferSize = 4096;
public readonly byte[] ReceiveBuffer = new byte[BufferSize]; public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new(1, 1); public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
public readonly SemaphoreSlim OnSentData = new(1, 1); public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false);
public readonly SemaphoreSlim OnReceivedData = new (1, 1); public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false);
public List<int> BytesReadPerSegment { get; set; } = new List<int>();
public List<int> BytesReadPerSegment { get; set; } = new(); public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
public SocketAsyncEventArgs SendEventArgs { get; set; } = new(); public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new();
public DateTime LastQuery { get; set; } = DateTime.Now; public DateTime LastQuery { get; set; } = DateTime.Now;
} }
internal class ConnectionUserToken
{
public Socket Socket { get; set; }
public CancellationToken CancellationToken { get; set; }
}
} }

View File

@ -0,0 +1,363 @@
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.RCon
{
/// <summary>
/// implementation of IRConConnection
/// </summary>
public class RConConnection : IRConConnection
{
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>();
public IPEndPoint Endpoint { get; private set; }
public string RConPassword { get; private set; }
private IRConParserConfiguration config;
private readonly ILogger _log;
private readonly Encoding _gameEncoding;
public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding)
{
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
_gameEncoding = gameEncoding;
RConPassword = password;
_log = log;
}
public void SetConfiguration(IRConParserConfiguration config)
{
this.config = config;
}
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
{
if (!ActiveQueries.ContainsKey(this.Endpoint))
{
ActiveQueries.TryAdd(this.Endpoint, new ConnectionState());
}
var connectionState = ActiveQueries[this.Endpoint];
#if DEBUG == true
_log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]");
#endif
// enter the semaphore so only one query is sent at a time per server.
await connectionState.OnComplete.WaitAsync();
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
if (timeSinceLastQuery < StaticHelpers.FloodProtectionInterval)
{
await Task.Delay(StaticHelpers.FloodProtectionInterval - (int)timeSinceLastQuery);
}
connectionState.LastQuery = DateTime.Now;
#if DEBUG == true
_log.WriteDebug($"Semaphore has been released [{this.Endpoint}]");
_log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]");
#endif
byte[] payload = null;
bool waitForResponse = config.WaitForResponse;
string convertEncoding(string text)
{
byte[] convertedBytes = Utilities.EncodingType.GetBytes(text);
return _gameEncoding.GetString(convertedBytes);
}
try
{
string convertedRConPassword = convertEncoding(RConPassword);
string convertedParameters = convertEncoding(parameters);
switch (type)
{
case StaticHelpers.QueryType.GET_DVAR:
waitForResponse |= true;
payload = string.Format(config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.SET_DVAR:
payload = string.Format(config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND:
payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_STATUS:
waitForResponse |= true;
payload = (config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_INFO:
waitForResponse |= true;
payload = (config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse |= true;
payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray();
break;
}
}
// this happens when someone tries to send something that can't be converted into a 7 bit character set
// e.g: emoji -> windows-1252
catch (OverflowException)
{
connectionState.OnComplete.Release(1);
throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}");
}
byte[][] response = null;
retrySend:
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = true,
Ttl = 100,
ExclusiveAddressUse = true,
})
{
connectionState.SendEventArgs.UserToken = socket;
connectionState.OnSentData.Reset();
connectionState.OnReceivedData.Reset();
connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear();
#if DEBUG == true
_log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})");
#endif
try
{
response = await SendPayloadAsync(payload, waitForResponse);
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
{
throw new NetworkException("Expected response but got 0 bytes back");
}
connectionState.ConnectionAttempts = 0;
}
catch
{
if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
{
await Task.Delay(StaticHelpers.FloodProtectionInterval);
goto retrySend;
}
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"].FormatExt(Endpoint));
}
finally
{
if (connectionState.OnComplete.CurrentCount == 0)
{
connectionState.OnComplete.Release(1);
}
}
}
if (response.Length == 0)
{
_log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]");
return new string[0];
}
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) :
_gameEncoding.GetString(response[0]) + '\n';
// note: not all games respond if the pasword is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
}
if (responseString.Contains("rcon_password"))
{
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]);
}
if (responseString.Contains(config.ServerNotRunningResponse))
{
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
}
string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value;
string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
if (headerSplit.Length != 2)
{
throw new NetworkException("Unexpected response header from server");
}
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
return splitResponse;
}
/// <summary>
/// reassembles broken status segments into the 'correct' ordering
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
/// </summary>
/// <param name="segments">array of segmented byte arrays</param>
/// <returns></returns>
public string ReassembleSegmentedStatus(byte[][] segments)
{
var splitStatusStrings = new List<string>();
foreach (byte[] segment in segments)
{
string responseString = _gameEncoding.GetString(segment, 0, segment.Length);
var statusHeaderMatch = config.StatusHeader.PatternMatcher.Match(responseString);
if (statusHeaderMatch.Success)
{
splitStatusStrings.Insert(0, responseString.TrimEnd('\0'));
}
else
{
splitStatusStrings.Add(responseString.Replace(config.CommandPrefixes.RConResponse, "").TrimEnd('\0'));
}
}
return string.Join("", splitStatusStrings);
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse)
{
var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
if (connectionState.ReceiveEventArgs.RemoteEndPoint == null &&
connectionState.SendEventArgs.RemoteEndPoint == null)
{
// setup the event handlers only once because we're reusing the event args
connectionState.SendEventArgs.Completed += OnDataSent;
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
connectionState.SendEventArgs.RemoteEndPoint = this.Endpoint;
connectionState.ReceiveEventArgs.RemoteEndPoint = this.Endpoint;
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server
bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending)
{
// the send has not been completed asyncronously
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout)))
{
rconSocket.Close();
throw new NetworkException("Timed out sending data", rconSocket);
}
}
if (!waitForResponse)
{
return new byte[0][];
}
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back
bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending)
{
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000)))
{
rconSocket.Close();
throw new NetworkException("Timed out waiting for response", rconSocket);
}
}
rconSocket.Close();
var responseList = new List<byte[]>();
int totalBytesRead = 0;
foreach (int bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)
.Take(bytesRead)
.ToArray());
totalBytesRead += bytesRead;
}
return responseList.ToArray();
}
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
_log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
#endif
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
return;
}
if (sender is Socket sock)
{
var state = ActiveQueries[this.Endpoint];
state.BytesReadPerSegment.Add(e.BytesTransferred);
try
{
// we still have available data so the payload was segmented
if (sock.Available > 0)
{
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred);
if (!sock.ReceiveAsync(state.ReceiveEventArgs))
{
#if DEBUG == true
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}");
#endif
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
else
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
catch (ObjectDisposedException)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
_log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}");
#endif
ActiveQueries[this.Endpoint].OnSentData.Set();
}
}
}

View File

@ -1,16 +0,0 @@
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RConParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
internal sealed class DynamicRConParser : BaseRConParser
{
public DynamicRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) : base(logger, parserRegexFactory)
{
}
}
}

View File

@ -1,15 +0,0 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RConParsers
{
/// <inheritdoc cref="IStatusResponse"/>
public class StatusResponse : IStatusResponse
{
public string Map { get; set; }
public string GameType { get; set; }
public string Hostname { get; set; }
public int? MaxClients { get; set; }
public EFClient[] Clients { get; set; }
}
}

View File

@ -7,24 +7,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RConParsers namespace IW4MAdmin.Application.RconParsers
{ {
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
private readonly ILogger _logger; public BaseRConParser(IParserRegexFactory parserRegexFactory)
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{ {
_logger = logger;
Configuration = new DynamicRConParserConfiguration(parserRegexFactory) Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
{ {
CommandPrefixes = new CommandPrefix() CommandPrefixes = new CommandPrefix()
@ -59,7 +50,6 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.AdditionalGroup, int.MaxValue);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.GametypeStatus.Pattern = ""; Configuration.GametypeStatus.Pattern = "";
@ -77,35 +67,17 @@ namespace IW4MAdmin.Application.RConParsers
public Game GameName { get; set; } = Game.COD; public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true; public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty"; public string Name { get; set; } = "Call of Duty";
public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{ {
command = command.FormatMessageForEngine(Configuration?.ColorCodeMapping); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token); return response.Skip(1).ToArray();
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
{ {
string[] lineSplit; string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit).TrimEnd('\0');
try
{
lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName, token);
}
catch
{
if (fallbackValue == null)
{
throw;
}
lineSplit = Array.Empty<string>();
}
var response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") || if (response.Contains("Unknown command") ||
@ -113,7 +85,7 @@ namespace IW4MAdmin.Application.RConParsers
{ {
if (fallbackValue != null) if (fallbackValue != null)
{ {
return new Dvar<T> return new Dvar<T>()
{ {
Name = dvarName, Name = dvarName,
Value = fallbackValue Value = fallbackValue
@ -123,19 +95,19 @@ namespace IW4MAdmin.Application.RConParsers
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
} }
var value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value; string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value;
var defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value; string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value;
var latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value; string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value;
string RemoveTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = RemoveTrailingColorCode(value); value = removeTrailingColorCode(value);
defaultValue = RemoveTrailingColorCode(defaultValue); defaultValue = removeTrailingColorCode(defaultValue);
latchedValue = RemoveTrailingColorCode(latchedValue); latchedValue = removeTrailingColorCode(latchedValue);
return new Dvar<T> return new Dvar<T>()
{ {
Name = dvarName, Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)), DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)), LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
@ -143,108 +115,60 @@ namespace IW4MAdmin.Application.RConParsers
}; };
} }
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default) public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection)
{ {
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action => string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
{ #if DEBUG
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (false, (string)null)
});
}
}, token);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
_logger.LogDebug("Status Response {Response}", string.Join(Environment.NewLine, response));
return new StatusResponse
{
Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
};
}
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern)
{
if (string.IsNullOrEmpty(groupPattern))
{
return default;
}
string value = null;
foreach (var line in response) foreach (var line in response)
{ {
var regex = Regex.Match(line, groupPattern); Console.WriteLine(line);
}
#endif
return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
}
private string MapFromStatus(string[] response)
{
string map = null;
foreach (var line in response)
{
var regex = Regex.Match(line, Configuration.MapStatus.Pattern);
if (regex.Success) if (regex.Success)
{ {
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString(); map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString();
} }
} }
if (value == null) return map;
{
return default;
}
if (typeof(T) == typeof(int?))
{
return (T)Convert.ChangeType(int.Parse(value), Nullable.GetUnderlyingType(typeof(T)));
}
return (T)Convert.ChangeType(value, typeof(T));
} }
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default) private string GameTypeFromStatus(string[] response)
{ {
var dvarString = (dvarValue is string str) if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
{
return null;
}
string gametype = null;
foreach (var line in response)
{
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern);
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
}
return gametype;
}
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{
string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\"" ? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue}"; : $"{dvarName} {dvarValue.ToString()}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
}
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
CancellationToken token = default)
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = false
});
}
}, token);
} }
private List<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)
@ -266,21 +190,10 @@ namespace IW4MAdmin.Application.RConParsers
if (match.Success) if (match.Success)
{ {
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]] == "ZMBI") int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
{ int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
_logger.LogDebug("Ignoring detected client {client} because they are zombie state", string.Join(",", match.Values));
continue;
}
var clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
var score = 0;
if (Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore] > 0)
{
score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
}
var ping = 999; int ping = 999;
// their state can be CNCT, ZMBI etc // their state can be CNCT, ZMBI etc
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3) if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
@ -289,22 +202,14 @@ namespace IW4MAdmin.Application.RConParsers
} }
long networkId; long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
.Contains(_botIpIndicator))
{
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
}
try try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ? networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
} }
@ -314,9 +219,11 @@ namespace IW4MAdmin.Application.RConParsers
continue; continue;
} }
var client = new EFClient int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
var client = new EFClient()
{ {
CurrentAlias = new EFAlias CurrentAlias = new EFAlias()
{ {
Name = name, Name = name,
IPAddress = ip IPAddress = ip
@ -330,17 +237,6 @@ namespace IW4MAdmin.Application.RConParsers
client.SetAdditionalProperty("BotGuid", networkIdString); client.SetAdditionalProperty("BotGuid", networkIdString);
if (Configuration.Status.GroupMapping.ContainsKey(ParserRegex.GroupType.AdditionalGroup))
{
var additionalGroupIndex =
Configuration.Status.GroupMapping[ParserRegex.GroupType.AdditionalGroup];
if (match.Values.Length > additionalGroupIndex)
{
client.SetAdditionalProperty("ConnectionClientId", match.Values[additionalGroupIndex]);
}
}
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }
@ -367,16 +263,5 @@ namespace IW4MAdmin.Application.RConParsers
public T GetDefaultDvarValue<T>(string dvarName) => Configuration.DefaultDvarValues.ContainsKey(dvarName) ? public T GetDefaultDvarValue<T>(string dvarName) => Configuration.DefaultDvarValues.ContainsKey(dvarName) ?
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) : (T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default; default;
public TimeSpan OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
{
return TimeSpan.FromSeconds(30);
}
return TimeSpan.Zero;
}
} }
} }

View File

@ -0,0 +1,15 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
sealed internal class DynamicRConParser : BaseRConParser
{
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
{
}
}
}

View File

@ -1,11 +1,9 @@
using System; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon; using SharedLibraryCore.RCon;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using SharedLibraryCore.Formatting;
namespace IW4MAdmin.Application.RConParsers namespace IW4MAdmin.Application.RconParsers
{ {
/// <summary> /// <summary>
/// generic implementation of the IRConParserConfiguration /// generic implementation of the IRConParserConfiguration
@ -17,8 +15,6 @@ namespace IW4MAdmin.Application.RConParsers
public ParserRegex Status { get; set; } public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; } public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; } public ParserRegex GametypeStatus { get; set; }
public ParserRegex HostnameStatus { get; set; }
public ParserRegex MaxPlayersStatus { get; set; }
public ParserRegex Dvar { get; set; } public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; } public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; } public string ServerNotRunningResponse { get; set; }
@ -26,28 +22,6 @@ namespace IW4MAdmin.Application.RConParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750;
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{
// this is the default mapping (IW4), but can be overridden as needed in the parsers
{ColorCodes.Black.ToString(), "^0"},
{ColorCodes.Red.ToString(), "^1"},
{ColorCodes.Green.ToString(), "^2"},
{ColorCodes.Yellow.ToString(), "^3"},
{ColorCodes.Blue.ToString(), "^4"},
{ColorCodes.Cyan.ToString(), "^5"},
{ColorCodes.Pink.ToString(), "^6"},
{ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), "^:"}
};
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {
@ -56,8 +30,6 @@ namespace IW4MAdmin.Application.RConParsers
GametypeStatus = parserRegexFactory.CreateParserRegex(); GametypeStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex(); Dvar = parserRegexFactory.CreateParserRegex();
StatusHeader = parserRegexFactory.CreateParserRegex(); StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
} }
} }
} }

View File

@ -1,10 +0,0 @@
using System;
namespace Data.Abstractions
{
public class IAuditFields
{
DateTime CreatedDateTime { get; set; }
DateTime? UpdatedDateTime { get; set; }
}
}

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Data.Abstractions
{
public interface IDataValueCache<TEntityType, TReturnType> where TEntityType : class
{
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Data.Abstractions
{
public interface ILookupCache<T> where T : class
{
Task InitializeAsync();
Task<T> AddAsync(T item);
Task<T> FirstAsync(Func<T, bool> query);
IEnumerable<T> GetAll();
}
}

View File

@ -1,13 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Abstractions
{
public interface IUniqueId
{
[NotMapped]
long Id { get; }
[NotMapped]
string Value { get; }
}
}

View File

@ -1,49 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Microsoft.EntityFrameworkCore;
namespace Data.Context
{
public static class ContextSeed
{
public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
{
await using var context = contextFactory.CreateContext();
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await context.Database.MigrateAsync(token);
});
if (!await context.AliasLinks.AnyAsync(token))
{
var link = new EFAliasLink();
context.Clients.Add(new EFClient
{
Active = false,
Connections = 0,
FirstConnection = DateTime.UtcNow,
LastConnection = DateTime.UtcNow,
Level = EFClient.Permission.Console,
Masked = true,
NetworkId = 0,
AliasLink = link,
CurrentAlias = new EFAlias
{
Link = link,
Active = true,
DateAdded = DateTime.UtcNow,
Name = "IW4MAdmin",
},
});
await context.SaveChangesAsync(token);
}
}
}
}

View File

@ -1,167 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading;
using System.Threading.Tasks;
using Data.Extensions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Client.Stats.Reference;
using Data.Models.Misc;
using Data.Models.Server;
namespace Data.Context
{
public abstract class DatabaseContext : DbContext
{
public DbSet<EFClient> Clients { get; set; }
public DbSet<EFAlias> Aliases { get; set; }
public DbSet<EFAliasLink> AliasLinks { get; set; }
public DbSet<EFPenalty> Penalties { get; set; }
public DbSet<EFPenaltyIdentifier> PenaltyIdentifiers { get; set; }
public DbSet<EFMeta> EFMeta { get; set; }
public DbSet<EFChangeHistory> EFChangeHistory { get; set; }
#region STATS
public DbSet<Models.Vector3> Vector3s { get; set; }
public DbSet<EFACSnapshotVector3> SnapshotVector3s { get; set; }
public DbSet<EFACSnapshot> ACSnapshots { get; set; }
public DbSet<EFServer> Servers { get; set; }
public DbSet<EFClientKill> ClientKills { get; set; }
public DbSet<EFClientMessage> ClientMessages { get; set; }
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
public DbSet<EFHitLocation> HitLocations { get; set; }
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
public DbSet<EFWeapon> Weapons { get; set; }
public DbSet<EFWeaponAttachment> WeaponAttachments { get; set; }
public DbSet<EFMap> Maps { get; set; }
#endregion
#region MISC
public DbSet<EFInboxMessage> InboxMessages { get; set; }
public DbSet<EFServerSnapshot> ServerSnapshots { get;set; }
public DbSet<EFClientConnectionHistory> ConnectionHistory { get; set; }
#endregion
private void SetAuditColumns()
{
return;
}
public DatabaseContext()
{
if (!MigrationExtensions.IsMigration)
{
throw new InvalidOperationException();
}
}
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
protected DatabaseContext(DbContextOptions options) : base(options)
{
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
SetAuditColumns();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override int SaveChanges()
{
SetAuditColumns();
return base.SaveChanges();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// make network id unique
modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
client.GameName
});
});
modelBuilder.Entity<EFPenalty>(entity =>
{
entity.HasOne(p => p.Offender)
.WithMany(c => c.ReceivedPenalties)
.HasForeignKey(c => c.OffenderId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(p => p.Punisher)
.WithMany(p => p.AdministeredPenalties)
.HasForeignKey(c => c.PunisherId)
.OnDelete(DeleteBehavior.Restrict);
entity.Property(p => p.Expires)
.IsRequired(false);
});
modelBuilder.Entity<EFAliasLink>(entity =>
{
entity.HasMany(e => e.Children)
.WithOne(a => a.Link)
.HasForeignKey(k => k.LinkId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<EFAlias>(ent =>
{
ent.Property(a => a.IPAddress).IsRequired(false);
ent.HasIndex(a => a.IPAddress);
ent.Property(a => a.Name).HasMaxLength(24);
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});
ent.Property(alias => alias.SearchableIPAddress)
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
ent.HasIndex(alias => alias.SearchableIPAddress);
});
modelBuilder.Entity<EFMeta>(ent =>
{
ent.HasIndex(_meta => _meta.Key);
ent.HasIndex(_meta => _meta.LinkedMetaId);
ent.HasOne(_meta => _meta.LinkedMeta)
.WithMany()
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity<EFPenaltyIdentifier>(ent =>
{
ent.HasIndex(identifiers => identifiers.NetworkId);
ent.HasIndex(identifiers => identifiers.IPv4Address);
});
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
// force full name for database conversion
modelBuilder.Entity<EFClient>().ToTable("EFClients");
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
modelBuilder.Entity<EFAliasLink>().ToTable("EFAliasLinks");
modelBuilder.Entity<EFPenalty>().ToTable("EFPenalties");
modelBuilder.Entity<EFPenaltyIdentifier>().ToTable("EFPenaltyIdentifiers");
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);
base.OnModelCreating(modelBuilder);
}
}
}

View File

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title>
<Authors />
<PackageVersion>1.2.0</PackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql" Version="6.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
</ItemGroup>
</Project>

View File

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
namespace Data.Extensions
{
public static class MigrationExtensions
{
public static bool IsMigration => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Migration";
}
}

Some files were not shown because too many files have changed in this diff Show More