Compare commits

..

No commits in common. "release/pre" and "2021.01.18.1-prerelease" have entirely different histories.

1087 changed files with 16229 additions and 145138 deletions

3
.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/*
@ -245,5 +246,3 @@ launchSettings.json
/GameLogServer/game_log_server_env /GameLogServer/game_log_server_env
.idea/* .idea/*
*.db *.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,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Plugin; using IW4MAdmin.Application.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestEase; using RestEase;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;

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,172 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
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();
private readonly SemaphoreSlim _onModifyingAlerts = new(1, 1);
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)
{
try
{
_onModifyingAlerts.Wait();
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).ToList();
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void MarkAlertAsRead(Guid alertId)
{
try
{
_onModifyingAlerts.Wait();
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);
}
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
try
{
_onModifyingAlerts.Wait();
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void AddAlert(Alert.AlertState alert)
{
try
{
_onModifyingAlerts.Wait();
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();
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
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,7 +2,7 @@
<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>2020.0.0.0</Version>
@ -24,21 +24,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2049" /> <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.10">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<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.10" />
<PackageReference Include="RestEase" Version="1.5.7" /> <PackageReference Include="RestEase" Version="1.5.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
@ -51,8 +48,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>
@ -64,10 +59,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Configuration\LoggingConfiguration.json"> <None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\GeoLite2-Country.mmdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>

View File

@ -1,11 +1,12 @@
using IW4MAdmin.Application.EventParsers; 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.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
@ -15,27 +16,15 @@ 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.Globalization;
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 Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog.Context; using Serilog.Context;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Formatting;
using SharedLibraryCore.Interfaces.Events;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger; using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@ -54,10 +43,8 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; } public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; } public IList<IEventParser> AdditionalEventParsers { get; }
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; } public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _isRunningTokenSource.Token; public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; } public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; } public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; } public IMiddlewareActionHandler MiddlewareActionHandler { get; }
@ -67,56 +54,55 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
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 IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private CancellationTokenSource _isRunningTokenSource; private readonly CancellationTokenSource _tokenSource;
private CancellationTokenSource _eventHandlerTokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>(); private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration; private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory; private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory; private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents; private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly ICoreEventHandler _coreEventHandler; private readonly IEventHandler _eventHandler;
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 IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService; private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new(); public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger<ApplicationManager> 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,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager;
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, _appConfig) };
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_isRunningTokenSource = new CancellationTokenSource(); _metaService = metaService;
_tokenSource = new CancellationTokenSource();
_commands = commands.ToList(); _commands = commands.ToList();
_translationLookup = translationLookup; _translationLookup = translationLookup;
_commandConfiguration = commandConfiguration; _commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory; _serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory; _parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents; _customParserEvents = customParserEvents;
_coreEventHandler = coreEventHandler; _eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver; _scriptPluginServiceResolver = scriptPluginServiceResolver;
@ -124,17 +110,13 @@ namespace IW4MAdmin.Application
_changeHistoryService = changeHistoryService; _changeHistoryService = changeHistoryService;
_appConfig = appConfig; _appConfig = appConfig;
Plugins = plugins; Plugins = plugins;
InteractionRegistration = interactionRegistration;
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
} }
public IEnumerable<IPlugin> Plugins { get; } public IEnumerable<IPlugin> Plugins { get; }
public IInteractionRegistration InteractionRegistration { get; }
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent); ProcessingEvents.TryAdd(newEvent.Id, newEvent);
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
@ -152,12 +134,12 @@ namespace IW4MAdmin.Application
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId); _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId); _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
} }
// this happens if a plugin requires login // this happens if a plugin requires login
@ -196,11 +178,11 @@ namespace IW4MAdmin.Application
} }
skip: skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null) if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{ {
var correlatedEvents = var correlatedEvents =
ProcessingEvents.Values.Where(ev => ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId) ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList(); .ToList();
await Task.WhenAll(correlatedEvents.Select(ev => await Task.WhenAll(correlatedEvents.Select(ev =>
@ -209,16 +191,14 @@ namespace IW4MAdmin.Application
foreach (var correlatedEvent in correlatedEvents) foreach (var correlatedEvent in correlatedEvents)
{ {
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _); ProcessingEvents.Remove(correlatedEvent.Id, out _);
} }
} }
// we don't want to remove events that are correlated to command // we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.Count(gameEvent => if (ProcessingEvents.Values.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
newEvent.CorrelationId is null)
{ {
ProcessingEvents.Remove(newEvent.IncrementalId, out _); ProcessingEvents.Remove(newEvent.Id, out _);
} }
// tell anyone waiting for the output that we're done // tell anyone waiting for the output that we're done
@ -236,60 +216,74 @@ namespace IW4MAdmin.Application
return _commands; return _commands;
} }
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList(); public async Task UpdateServerStates()
private Task UpdateServerStates()
{ {
var index = 0; // store the server hash code and task for it
return Task.WhenAll(_servers.Select(server => var runningUpdateTasks = new Dictionary<long, Task>();
{
var thisIndex = index;
Interlocked.Increment(ref index);
return ProcessUpdateHandler(server, thisIndex);
}));
}
private async Task ProcessUpdateHandler(Server server, int index) while (!_tokenSource.IsCancellationRequested)
{
const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
var timeout = TimeSpan.FromMinutes(2);
while (!_isRunningTokenSource.IsCancellationRequested)
{ {
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled ||
ut.Value.Status == TaskStatus.Faulted)
.Select(ut => ut.Key)
.ToList();
// this is to prevent the log reader from starting before the initial
// query of players on the server
if (serverTasksToRemove.Count > 0)
{
IsInitialized = true;
}
// remove the update tasks as they have completd
foreach (long serverId in serverTasksToRemove)
{
runningUpdateTasks.Remove(serverId);
}
// select the servers where the tasks have completed
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)))
{
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
catch (Exception e)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(e, "Failed to update status");
}
}
finally
{
server.IsInitialized = true;
}
}));
}
try try
{ {
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index); await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
await Task.Delay(delayFactor, _isRunningTokenSource.Token);
using var timeoutTokenSource = new CancellationTokenSource();
timeoutTokenSource.CancelAfter(timeout);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
_isRunningTokenSource.Token);
await server.ProcessUpdatesAsync(linkedTokenSource.Token);
await Task.Delay(Math.Max(1000, _appConfig.RConPollRate - delayFactor),
_isRunningTokenSource.Token);
} }
catch (OperationCanceledException) // if a cancellation is received, we want to return immediately after shutting down
catch
{ {
// ignored foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.Id))
{ {
_logger.LogError(ex, "Failed to update status"); await server.ProcessUpdatesAsync(_tokenSource.Token);
} }
} break;
finally
{
server.IsInitialized = true;
} }
} }
// run the final updates to clean up server
await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
} }
public async Task Init() public async Task Init()
@ -297,37 +291,19 @@ 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>(), _isRunningTokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region EVENTS
IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
IGameServerEventSubscriptions.ServerCommandExecuteRequested += OnServerCommandExecuteRequested;
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
# endregion
#region PLUGINS #region PLUGINS
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
try try
{ {
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser) if (plugin is ScriptPlugin scriptPlugin)
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver, await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
scriptPlugin.Watcher.Changed += async (sender, e) => scriptPlugin.Watcher.Changed += async (sender, e) =>
{ {
try try
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver, await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
} }
catch (Exception ex) catch (Exception ex)
@ -355,13 +331,15 @@ namespace IW4MAdmin.Application
// copy over default config if it doesn't exist // copy over default config if it doesn't exist
if (!_appConfig.Servers?.Any() ?? true) if (!_appConfig.Servers?.Any() ?? true)
{ {
var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings"); var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
await defaultHandler.BuildAsync(); //ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var defaultConfig = defaultHandler.Configuration(); //var newConfig = ConfigHandler.Configuration();
_appConfig.AutoMessages = defaultConfig.AutoMessages; _appConfig.AutoMessages = defaultConfig.AutoMessages;
_appConfig.GlobalRules = defaultConfig.GlobalRules; _appConfig.GlobalRules = defaultConfig.GlobalRules;
_appConfig.Maps = defaultConfig.Maps;
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames; _appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
_appConfig.QuickMessages = defaultConfig.QuickMessages;
//if (newConfig.Servers == null) //if (newConfig.Servers == null)
{ {
@ -393,31 +371,21 @@ namespace IW4MAdmin.Application
if (string.IsNullOrEmpty(_appConfig.Id)) if (string.IsNullOrEmpty(_appConfig.Id))
{ {
_appConfig.Id = Guid.NewGuid().ToString(); _appConfig.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
} }
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl)) if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{ {
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624"; _appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
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(_appConfig);
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
@ -426,7 +394,7 @@ namespace IW4MAdmin.Application
foreach (var serverConfig in _appConfig.Servers) foreach (var serverConfig in _appConfig.Servers)
{ {
ConfigurationMigration.ModifyLogPath020919(serverConfig); Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null) if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{ {
@ -442,8 +410,8 @@ namespace IW4MAdmin.Application
serverConfig.ModifyParsers(); serverConfig.ModifyParsers();
} }
await ConfigHandler.Save();
} }
await ConfigHandler.Save();
} }
if (_appConfig.Servers.Length == 0) if (_appConfig.Servers.Length == 0)
@ -454,21 +422,19 @@ namespace IW4MAdmin.Application
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(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
foreach (var parser in AdditionalRConParsers) #endregion
{
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
: "");
}
}
#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 #endregion
#region COMMANDS #region COMMANDS
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token)) if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -490,17 +456,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 = _appConfig.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = _appConfig?.BroadcastCommandPrefix ?? "@"; cmdConfig.BroadcastCommandPrefix = _appConfig.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,
@ -515,7 +477,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))
@ -529,7 +490,6 @@ namespace IW4MAdmin.Application
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]); Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
IsInitialized = true;
} }
private async Task InitializeServers() private async Task InitializeServers()
@ -543,23 +503,26 @@ namespace IW4MAdmin.Application
try try
{ {
// 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())) using (LogContext.PushProperty("Server", ServerInstance.ToString()))
{ {
_logger.LogInformation("Beginning server communication initialization"); _logger.LogInformation("Beginning server communication initialization");
await serverInstance.Initialize(); await ServerInstance.Initialize();
_servers.Add(serverInstance); _servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors())); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname); _logger.LogInformation("Finishing initialization and now monitoring [{server}]", ServerInstance.Hostname, ServerInstance.ToString());
} }
QueueEvent(new MonitorStartEvent // add the start event for this server
var e = new GameEvent()
{ {
Server = serverInstance, Type = GameEvent.EventType.Start,
Source = this Data = $"{ServerInstance.GameName} started",
}); Owner = ServerInstance
};
AddEvent(e);
successServers++; successServers++;
} }
@ -581,71 +544,27 @@ namespace IW4MAdmin.Application
throw lastException; throw lastException;
} }
if (successServers != config.Servers.Length && !AppContext.TryGetSwitch("NoConfirmPrompt", out _)) if (successServers != config.Servers.Length)
{ {
if (!Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"].PromptBool()) if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{ {
throw lastException; throw lastException;
} }
} }
} }
public async Task Start() public async Task Start() => await UpdateServerStates();
public void Stop()
{ {
_eventHandlerTokenSource = new CancellationTokenSource(); _tokenSource.Cancel();
var eventHandlerThread = new Thread(() =>
{
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
})
{
Name = nameof(CoreEventHandler)
};
eventHandlerThread.Start();
await UpdateServerStates();
_eventHandlerTokenSource.Cancel();
eventHandlerThread.Join();
}
public async Task Stop()
{
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
{
try
{
await plugin.OnUnloadAsync().WithTimeout(Utilities.DefaultCommandTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
}
}
_isRunningTokenSource.Cancel();
IsRunning = false; IsRunning = false;
} }
public async Task Restart() public void Restart()
{ {
IsRestartRequested = true; IsRestartRequested = true;
await Stop(); Stop();
using var subscriptionTimeoutToken = new CancellationTokenSource();
subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
IGameEventSubscriptions.ClearEventInvocations();
IGameServerEventSubscriptions.ClearEventInvocations();
IManagementEventSubscriptions.ClearEventInvocations();
_isRunningTokenSource.Dispose();
_isRunningTokenSource = new CancellationTokenSource();
_eventHandlerTokenSource.Dispose();
_eventHandlerTokenSource = new CancellationTokenSource();
} }
[Obsolete] [Obsolete]
@ -665,11 +584,6 @@ 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;
@ -687,12 +601,7 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent) public void AddEvent(GameEvent gameEvent)
{ {
_coreEventHandler.QueueEvent(this, gameEvent); _eventHandler.HandleEvent(this, gameEvent);
}
public void QueueEvent(CoreEvent coreEvent)
{
_coreEventHandler.QueueEvent(this, coreEvent);
} }
public IPageList GetPageList() public IPageList GetPageList()
@ -729,166 +638,14 @@ namespace IW4MAdmin.Application
public void AddAdditionalCommand(IManagerCommand command) public void AddAdditionalCommand(IManagerCommand command)
{ {
lock (_commands) if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
{ {
if (_commands.Any(cmd => cmd.Name == command.Name || cmd.Alias == command.Alias)) throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
{
throw new InvalidOperationException(
$"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_commands.Add(command);
} }
_commands.Add(command);
} }
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;
private async Task OnServerValueRequested(ServerValueRequestEvent requestEvent, CancellationToken token)
{
if (requestEvent.Server is not IW4MServer server)
{
return;
}
Dvar<string> serverValue = null;
try
{
if (requestEvent.DelayMs.HasValue)
{
await Task.Delay(requestEvent.DelayMs.Value, token);
}
var waitToken = token;
using var timeoutTokenSource = new CancellationTokenSource();
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
if (requestEvent.TimeoutMs is not null)
{
timeoutTokenSource.CancelAfter(requestEvent.TimeoutMs.Value);
waitToken = linkedTokenSource.Token;
}
serverValue =
await server.GetDvarAsync(requestEvent.ValueName, requestEvent.FallbackValue, waitToken);
}
catch
{
// ignored
}
finally
{
QueueEvent(new ServerValueReceiveEvent
{
Server = server,
Source = server,
Response = serverValue ?? new Dvar<string> { Name = requestEvent.ValueName },
Success = serverValue is not null
});
}
}
private Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
{
return ExecuteWrapperForServerQuery(requestEvent, token, async (innerEvent) =>
{
if (innerEvent.DelayMs.HasValue)
{
await Task.Delay(innerEvent.DelayMs.Value, token);
}
if (innerEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await innerEvent.Server.SetDvarAsync(innerEvent.ValueName, innerEvent.Value, token);
}, (completed, innerEvent) =>
{
QueueEvent(new ServerValueSetCompleteEvent
{
Server = innerEvent.Server,
Source = innerEvent.Server,
Success = completed,
Value = innerEvent.Value,
ValueName = innerEvent.ValueName
});
return Task.CompletedTask;
});
}
private Task OnServerCommandExecuteRequested(ServerCommandRequestExecuteEvent executeEvent, CancellationToken token)
{
return ExecuteWrapperForServerQuery(executeEvent, token, async (innerEvent) =>
{
if (innerEvent.DelayMs.HasValue)
{
await Task.Delay(innerEvent.DelayMs.Value, token);
}
if (innerEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await innerEvent.Server.ExecuteCommandAsync(innerEvent.Command, token);
}, (_, __) => Task.CompletedTask);
}
private async Task ExecuteWrapperForServerQuery<TEventType>(TEventType serverEvent, CancellationToken token,
Func<TEventType, Task> action, Func<bool, TEventType, Task> complete) where TEventType : GameServerEvent
{
if (serverEvent.Server is not IW4MServer)
{
return;
}
var completed = false;
try
{
await action(serverEvent);
completed = true;
}
catch
{
// ignored
}
finally
{
await complete(completed, serverEvent);
}
}
private async Task OnClientPersistentIdReceived(ClientPersistentIdReceiveEvent receiveEvent, CancellationToken token)
{
var parts = receiveEvent.PersistentId.Split(",");
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
var penalties = await PenaltySvc
.GetActivePenaltiesByIdentifier(null, guid, receiveEvent.Client.GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && receiveEvent.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
{
_logger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
receiveEvent.Client, guid);
receiveEvent.Client.Ban(_translationLookup["SERVER_BAN_EVADE"].FormatExt(guid),
receiveEvent.Client.CurrentServer.AsConsoleClient(), true);
}
}
}
} }
} }

View File

@ -7,6 +7,6 @@ foreach($localization in $localizations)
{ {
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization $url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization $filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
$response = Invoke-WebRequest $url -UseBasicParsing $response = Invoke-WebRequest $url
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8 Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
} }

View File

@ -14,4 +14,3 @@ if not exist "%TargetDir%Plugins" (
) )
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\" xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
del "%TargetDir%Plugins\SQLite*"

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,52 +0,0 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Meta;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class AddClientNoteCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
{
Name = "addnote";
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
Alias = "an";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
Required = false
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var note = new ClientNoteMetaResponse
{
Note = gameEvent.Data?.Trim(),
OriginEntityId = gameEvent.Origin.ClientId,
ModifiedDate = DateTime.UtcNow
};
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
}
}

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,95 +0,0 @@
using System;
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>
/// 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 async 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();
var messageList = new List<string>();
foreach (var item in commandStrings)
{
helpResponse.Append(item.response);
if (item.index == 0 || item.index % 4 != 0)
{
continue;
}
messageList.Add(helpResponse.ToString());
helpResponse = new StringBuilder();
}
messageList.Add(helpResponse.ToString());
await gameEvent.Origin.TellAsync(messageList);
}
}
}
}

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,72 +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.User;
_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;
}
await gameEvent.Origin.TellAsync(inboxItems.Select((inboxItem, index) =>
{
var header = _translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index + 1}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name);
return new[] { header }.Union(inboxItem.Message.FragmentMessageForDisplay());
}).SelectMany(item => item));
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,80 +0,0 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using Serilog.Core;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class SetLogLevelCommand : Command
{
private readonly Func<string, LoggingLevelSwitch> _levelSwitchResolver;
public SetLogLevelCommand(CommandConfiguration config, ITranslationLookup layout, Func<string, LoggingLevelSwitch> levelSwitchResolver) : base(config, layout)
{
_levelSwitchResolver = levelSwitchResolver;
Name = "loglevel";
Alias = "ll";
Description = "set minimum logging level";
Permission = EFClient.Permission.Owner;
Arguments = new CommandArgument[]
{
new()
{
Name = "Log Level",
Required = true
},
new()
{
Name = "Override",
Required = false
},
new()
{
Name = "IsDevelopment",
Required = false
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var args = gameEvent.Data.Split(" ");
if (!Enum.TryParse<LogEventLevel>(args[0], out var minLevel))
{
await gameEvent.Origin.TellAsync(new[]
{
$"Valid log values: {string.Join(",", Enum.GetValues<LogEventLevel>())}"
});
return;
}
var context = string.Empty;
if (args.Length > 1)
{
context = args[1];
}
var loggingSwitch = _levelSwitchResolver(context);
loggingSwitch.MinimumLevel = minLevel;
if (args.Length > 2 && (args[2] == "1" || args[2].ToLower() == "true"))
{
AppContext.SetSwitch("IsDevelop", true);
}
else
{
AppContext.SetSwitch("IsDevelop", false);
}
await gameEvent.Origin.TellAsync(new[]
{ $"Set minimum log level to {loggingSwitch.MinimumLevel.ToString()}" });
}
}

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

@ -3,13 +3,7 @@
"Using": [ "Using": [
"Serilog.Sinks.File" "Serilog.Sinks.File"
], ],
"MinimumLevel": { "MinimumLevel": "Information",
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [ "WriteTo": [
{ {
"Name": "File", "Name": "File",
@ -18,13 +12,6 @@
"rollingInterval": "Day", "rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}" "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": [ "Enrich": [

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

View File

@ -1,145 +0,0 @@
using System;
using System.Collections.Concurrent;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application
{
public class CoreEventHandler : ICoreEventHandler
{
private const int MaxCurrentEvents = 25;
private readonly ILogger _logger;
private readonly SemaphoreSlim _onProcessingEvents = new(MaxCurrentEvents, MaxCurrentEvents);
private readonly ManualResetEventSlim _onEventReady = new(false);
private readonly ConcurrentQueue<(IManager, CoreEvent)> _runningEventTasks = new();
private CancellationToken _cancellationToken;
private int _activeTasks;
private static readonly GameEvent.EventType[] OverrideEvents =
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public CoreEventHandler(ILogger<CoreEventHandler> logger)
{
_logger = logger;
}
public void QueueEvent(IManager manager, CoreEvent coreEvent)
{
_runningEventTasks.Enqueue((manager, coreEvent));
_onEventReady.Set();
}
public void StartProcessing(CancellationToken token)
{
_cancellationToken = token;
while (!_cancellationToken.IsCancellationRequested)
{
_onEventReady.Reset();
try
{
_onProcessingEvents.Wait(_cancellationToken);
if (!_runningEventTasks.TryDequeue(out var coreEvent))
{
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
{
_onProcessingEvents.Release(1);
}
_onEventReady.Wait(_cancellationToken);
continue;
}
_logger.LogDebug("Start processing event {Name} {SemaphoreCount} - {QueuedTasks}",
coreEvent.Item2.GetType().Name, _onProcessingEvents.CurrentCount, _runningEventTasks.Count);
_ = Task.Factory.StartNew(() =>
{
Interlocked.Increment(ref _activeTasks);
_logger.LogDebug("[Start] Active Tasks = {TaskCount}", _activeTasks);
return HandleEventTaskExecute(coreEvent);
});
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not enqueue event for processing");
}
}
}
private async Task HandleEventTaskExecute((IManager, CoreEvent) coreEvent)
{
try
{
await GetEventTask(coreEvent.Item1, coreEvent.Item2);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Event timed out {Type}", coreEvent.Item2.GetType().Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete invoke for {EventType}",
coreEvent.Item2.GetType().Name);
}
finally
{
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
{
_logger.LogDebug("Freeing up event semaphore for next event {SemaphoreCount}",
_onProcessingEvents.CurrentCount);
_onProcessingEvents.Release(1);
}
Interlocked.Decrement(ref _activeTasks);
_logger.LogDebug("[Complete] {Type}, Active Tasks = {TaskCount} - {Queue}", coreEvent.Item2.GetType(),
_activeTasks, _runningEventTasks.Count);
}
}
private Task GetEventTask(IManager manager, CoreEvent coreEvent)
{
return coreEvent switch
{
GameEvent gameEvent => BuildLegacyEventTask(manager, coreEvent, gameEvent),
GameServerEvent gameServerEvent => IGameServerEventSubscriptions.InvokeEventAsync(gameServerEvent,
manager.CancellationToken),
ManagementEvent managementEvent => IManagementEventSubscriptions.InvokeEventAsync(managementEvent,
manager.CancellationToken),
_ => Task.CompletedTask
};
}
private async Task BuildLegacyEventTask(IManager manager, CoreEvent coreEvent, GameEvent gameEvent)
{
if (manager.IsRunning || OverrideEvents.Contains(gameEvent.Type))
{
await manager.ExecuteEvent(gameEvent);
await IGameEventSubscriptions.InvokeEventAsync(coreEvent, manager.CancellationToken);
return;
}
_logger.LogDebug("Skipping event as we're shutting down {EventId}", gameEvent.IncrementalId);
}
}
}

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 Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game;
using static System.Int32;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,26 +13,19 @@ namespace IW4MAdmin.Application.EventParsers
{ {
public class BaseEventParser : IEventParser public class BaseEventParser : IEventParser
{ {
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
_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, public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
ApplicationConfiguration appConfig)
{ {
_customEventRegistrations = _customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger; _logger = logger;
_appConfig = appConfig; _appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory) Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{ {
GameDirectory = "main", GameDirectory = "main",
LocalizeText = "\x15",
}; };
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$"; Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
@ -57,15 +47,7 @@ namespace IW4MAdmin.Application.EventParsers
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.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.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.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);
@ -80,8 +62,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 = 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]|_)+)$";
@"^(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.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);
@ -96,32 +77,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.SayTeam },
{ "chat", GameEvent.EventType.Say },
{ "chatteam", GameEvent.EventType.SayTeam },
{ "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; }
@ -141,14 +97,15 @@ namespace IW4MAdmin.Application.EventParsers
if (timeMatch.Success) if (timeMatch.Success)
{ {
if (timeMatch.Values[0].Contains(':')) if (timeMatch.Values[0].Contains(":"))
{ {
gameTime = timeMatch gameTime = timeMatch
.Values .Values
.Skip(2) .Skip(2)
// this converts the timestamp into seconds passed // this converts the timestamp into seconds passed
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1)) .Select((_value, index) => long.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Sum(); .Sum();
} }
else else
{ {
@ -156,71 +113,257 @@ namespace IW4MAdmin.Application.EventParsers
} }
// we want to strip the time from the log line // we want to strip the time from the log line
logLine = logLine[timeMatch.Values.First().Length..].Trim(); logLine = logLine.Substring(timeMatch.Values.First().Length);
} }
var (eventType, eventKey) = GetEventTypeFromLine(logLine); string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
switch (eventType) if (eventType == "say" || eventType == "sayteam")
{ {
case GameEvent.EventType.Say or GameEvent.EventType.SayTeam: var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
return ParseMessageEvent(logLine, gameTime, eventType) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Kill:
return ParseKillEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Damage:
return ParseDamageEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreConnect:
return ParseClientEnterMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.JoinTeam:
return ParseJoinTeamEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreDisconnect:
return ParseClientExitMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.MapEnd:
return ParseMatchEndEvent(logLine, gameTime);
case GameEvent.EventType.MapChange:
return ParseMatchStartEvent(logLine, gameTime);
}
if (logLine.StartsWith("GSE;")) if (matchResult.Success)
{
return new GameScriptEvent
{ {
ScriptData = logLine, string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
}
if (eventType == "K")
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == "D")
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == "J")
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == "Q")
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType.Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log Source = GameEvent.EventSource.Log
}; };
} }
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey)) if (eventType.Contains("InitGame"))
{ {
return GenerateDefaultEvent(logLine, gameTime); string dump = eventType.Replace("InitGame: ", "");
}
var eventModifier = _customEventRegistrations[eventKey]; return new GameEvent()
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{ {
Type = GameEvent.EventType.Other, Type = GameEvent.EventType.MapChange,
Data = logLine, Data = logLine,
Subtype = eventModifier.Item1, Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log Source = GameEvent.EventSource.Log
}); };
} }
catch (Exception ex) if (_customEventRegistrations.ContainsKey(eventType))
{ {
_logger.LogError(ex, "Could not handle custom log event generation"); var eventModifier = _customEventRegistrations[eventType];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception e)
{
_logger.LogError(e, $"Could not handle custom event generation");
}
} }
return GenerateDefaultEvent(logLine, gameTime); return new GameEvent()
}
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
{
return new GameEvent
{ {
Type = GameEvent.EventType.Unknown, Type = GameEvent.EventType.Unknown,
Data = logLine, Data = logLine,
@ -232,455 +375,8 @@ namespace IW4MAdmin.Application.EventParsers
}; };
} }
private static GameEvent ParseMatchStartEvent(string logLine, long gameTime)
{
var dump = logLine.Replace("InitGame: ", "").DictionaryFromKeyValue();
return new MatchStartEvent
{
Type = GameEvent.EventType.MapChange,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump,
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = dump
};
}
private static GameEvent ParseMatchEndEvent(string logLine, long gameTime)
{
return new MatchEndEvent
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = logLine
};
}
private GameEvent ParseClientExitMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientExitMatchEvent
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
private GameEvent ParseJoinTeamEvent(string logLine, long gameTime)
{
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var clientSlotNumber =
Parse(match.Values[
Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientJoinTeamEvent
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = clientSlotNumber,
State = EFClient.ClientState.Connected,
},
Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
TeamName = team,
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientSlotNumber
};
}
private GameEvent ParseClientEnterMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]
.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientEnterMatchEvent
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
#region DAMAGE
private GameEvent ParseDamageEvent(string logLine, long gameTime)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientDamageEvent
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
private GameEvent ParseKillEvent(string logLine, long gameTime)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientKillEvent
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
#endregion
#region MESSAGE
private GameEvent ParseMessageEvent(string logLine, long gameTime, GameEvent.EventType eventType)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (!matchResult.Success)
{
return null;
}
var message = new string(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Where(c => !char.IsControl(c)).ToArray());
if (message.StartsWith("/"))
{
message = message[1..];
}
if (String.IsNullOrEmpty(message))
{
return null;
}
var originIdString =
matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var clientNumber =
Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new ClientCommandEvent
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
return new ClientMessageEvent
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
#endregion
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);
}
/// <inheritdoc/> /// <inheritdoc/>
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
{ {
if (string.IsNullOrWhiteSpace(eventSubtype)) if (string.IsNullOrWhiteSpace(eventSubtype))
{ {

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,36 +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 string LocalizeText { 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 SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using IW4MAdmin.Application.Plugin.Script;
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,28 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
namespace IW4MAdmin.Application.Extensions;
public static class ScriptPluginExtensions
{
public static IEnumerable<object> GetClientsBasicData(
this DbSet<Data.Models.Client.EFClient> set, int[] clientIds)
{
return set.Where(client => clientIds.Contains(client.ClientId))
.Select(client => new
{
client.ClientId,
client.CurrentAlias,
client.Level,
client.NetworkId
}).ToList();
}
public static IEnumerable<object> GetClientsStatData(this DbSet<EFClientStatistics> set, int[] clientIds,
double serverId)
{
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
}
}

View File

@ -1,27 +1,23 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Data.MigrationContext;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.MigrationContext;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace IW4MAdmin.Application.Extensions namespace IW4MAdmin.Application.Extensions
{ {
public static class StartupExtensions public static class StartupExtensions
{ {
private static ILogger _defaultLogger; private static ILogger _defaultLogger = null;
private static readonly LoggingLevelSwitch LevelSwitch = new();
private static readonly LoggingLevelSwitch MicrosoftLevelSwitch = new();
private static readonly LoggingLevelSwitch SystemLevelSwitch = new();
public static IServiceCollection AddBaseLogger(this IServiceCollection services, public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig) ApplicationConfiguration appConfig)
@ -33,37 +29,22 @@ namespace IW4MAdmin.Application.Extensions
.Build(); .Build();
var loggerConfig = new LoggerConfiguration() var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration); .ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
LevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Default"]);
MicrosoftLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:Microsoft"]);
SystemLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:System"]);
loggerConfig = loggerConfig.MinimumLevel.ControlledBy(LevelSwitch);
loggerConfig = loggerConfig.MinimumLevel.Override("Microsoft", MicrosoftLevelSwitch)
.MinimumLevel.Override("System", SystemLevelSwitch);
if (Utilities.IsDevelopment) if (Utilities.IsDevelopment)
{ {
loggerConfig = loggerConfig.WriteTo.Console( loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate: outputTemplate:
"[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}") "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Debug(); .MinimumLevel.Debug();
} }
_defaultLogger = loggerConfig.CreateLogger(); _defaultLogger = loggerConfig.CreateLogger();
} }
services.AddSingleton((string context) =>
{
return context.ToLower() switch
{
"microsoft" => MicrosoftLevelSwitch,
"system" => SystemLevelSwitch,
_ => LevelSwitch
};
});
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true)); services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory() services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true)); .AddSerilog(_defaultLogger, true));
@ -86,10 +67,10 @@ namespace IW4MAdmin.Application.Extensions
{DataSource = Path.Join(currentPath, "Database", "Database.db")}; {DataSource = Path.Join(currentPath, "Database", "Database.db")};
var connectionString = connectionStringBuilder.ToString(); var connectionString = connectionStringBuilder.ToString();
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<SqliteDatabaseContext>() var builder = new DbContextOptionsBuilder<SqliteDatabaseContext>()
.UseSqlite(connectionString) .UseSqlite(connectionString);
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>())
.EnableSensitiveDataLogging().Options); services.AddSingleton((DbContextOptions) builder.Options);
return services; return services;
} }
@ -98,10 +79,8 @@ namespace IW4MAdmin.Application.Extensions
case "mysql": case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout", var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase); StringComparison.InvariantCultureIgnoreCase);
var connectionString =
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : "");
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>() services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), .UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
mysqlOptions => mysqlOptions.EnableRetryOnFailure()) mysqlOptions => mysqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options); .UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services; return services;
@ -111,11 +90,7 @@ namespace IW4MAdmin.Application.Extensions
services.AddSingleton(sp => services.AddSingleton(sp =>
(DbContextOptions) new DbContextOptionsBuilder<PostgresqlDatabaseContext>() (DbContextOptions) new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""), .UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions => postgresqlOptions => postgresqlOptions.EnableRetryOnFailure())
{
postgresqlOptions.EnableRetryOnFailure();
postgresqlOptions.SetPostgresVersion(new Version("12.9"));
})
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options); .UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services; return services;
default: default:

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,9 @@
using System; using System;
using Data.Abstractions;
using Data.Context;
using Data.MigrationContext;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.MigrationContext;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {

View File

@ -18,22 +18,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<GameLogReaderHttp>>());
_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<GameLogReader>>());
_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,6 +1,4 @@
using System; using System;
using Data.Abstractions;
using Data.Models.Server;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
@ -14,7 +12,7 @@ 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 IMetaService _metaService;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
/// <summary> /// <summary>
@ -23,7 +21,7 @@ namespace IW4MAdmin.Application.Factories
/// <param name="translationLookup"></param> /// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param> /// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaServiceV2 metaService, IMetaService metaService,
IServiceProvider serviceProvider) IServiceProvider serviceProvider)
{ {
_translationLookup = translationLookup; _translationLookup = translationLookup;
@ -39,10 +37,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(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>());
_serviceProvider.GetRequiredService<CommandConfiguration>(), _translationLookup, _metaService,
_serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(),
_serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
} }
} }
} }

View File

@ -1,13 +1,7 @@
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 Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -16,31 +10,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<RConConnection> _logger;
/// <summary> /// <summary>
/// Base constructor /// Base constructor
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger"></param>
public RConConnectionFactory(IServiceProvider serviceProvider) public RConConnectionFactory(ILogger<RConConnection> 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

@ -1,15 +1,14 @@
using SharedLibraryCore; using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Linq;
using Data.Models;
using Data.Models.Client;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -31,12 +30,17 @@ namespace IW4MAdmin.Application.Factories
/// <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<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.Game> supportedGames) bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
{ {
var permissionEnum = Enum.Parse<EFClient.Permission>(permission); var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument
{
Name = _arg.Item1,
Required = _arg.Item2
}).ToArray();
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction, return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames); _config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>());
} }
} }
} }

View File

@ -0,0 +1,43 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application
{
public class GameEventHandler : IEventHandler
{
private readonly EventLog _eventLog;
private readonly ILogger _logger;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public GameEventHandler(ILogger<GameEventHandler> logger)
{
_eventLog = new EventLog();
_logger = logger;
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
EventApi.OnGameEvent(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
}
else
{
_logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id);
}
}
}
}

View File

@ -1,213 +0,0 @@
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.IO;
public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHandlerV2<TConfigurationType>
where TConfigurationType : class
{
private readonly ILogger<BaseConfigurationHandlerV2<TConfigurationType>> _logger;
private readonly ConfigurationWatcher _watcher;
private readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter()
},
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private readonly SemaphoreSlim _onIo = new(1, 1);
private TConfigurationType _configurationInstance;
private string _path = string.Empty;
private event Action<string> FileUpdated;
public BaseConfigurationHandlerV2(ILogger<BaseConfigurationHandlerV2<TConfigurationType>> logger,
ConfigurationWatcher watcher)
{
_logger = logger;
_watcher = watcher;
FileUpdated += OnFileUpdated;
}
~BaseConfigurationHandlerV2()
{
FileUpdated -= OnFileUpdated;
_watcher.Unregister(_path);
}
public async Task<TConfigurationType> Get(string configurationName,
TConfigurationType defaultConfiguration = default)
{
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
var cleanName = configurationName.Replace("\\", "").Replace("/", "");
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
_path = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{cleanName}.json");
TConfigurationType readConfiguration = null;
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
_watcher.Register(_path, FileUpdated);
if (readConfiguration is null)
{
_logger.LogError("Could not parse configuration {Type} at {FileName}", typeof(TConfigurationType).Name,
_path);
return defaultConfiguration;
}
}
catch (FileNotFoundException)
{
if (defaultConfiguration is not null)
{
await InternalSet(defaultConfiguration, false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not read configuration file at {Path}", _path);
return defaultConfiguration;
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
return _configurationInstance ??= readConfiguration;
}
public async Task Set(TConfigurationType configuration)
{
await InternalSet(configuration, true);
}
public async Task Set()
{
if (_configurationInstance is not null)
{
await InternalSet(_configurationInstance, true);
}
}
public event Action<TConfigurationType> Updated;
private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore)
{
try
{
if (awaitSemaphore)
{
await _onIo.WaitAsync();
}
await using var fileStream = File.Create(_path);
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
await fileStream.DisposeAsync();
_configurationInstance = configuration;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save configuration {Type} {Path}", configuration.GetType().Name, _path);
}
finally
{
if (awaitSemaphore && _onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private async void OnFileUpdated(string filePath)
{
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
var readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
if (readConfiguration is null)
{
_logger.LogWarning("Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
else
{
CopyUpdatedProperties(readConfiguration);
Updated?.Invoke(readConfiguration);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private void CopyUpdatedProperties(TConfigurationType newConfiguration)
{
if (_configurationInstance is null)
{
_configurationInstance = newConfiguration;
return;
}
_logger.LogDebug("Updating existing config with new values {Type} at {Path}", typeof(TConfigurationType).Name,
_path);
if (_configurationInstance is IDictionary configDict && newConfiguration is IDictionary newConfigDict)
{
configDict.Clear();
foreach (var key in newConfigDict.Keys)
{
configDict.Add(key, newConfigDict[key]);
}
}
else
{
foreach (var property in _configurationInstance.GetType().GetProperties()
.Where(prop => prop.CanRead && prop.CanWrite))
{
property.SetValue(_configurationInstance, property.GetValue(newConfiguration));
}
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharedLibraryCore;
namespace IW4MAdmin.Application.IO;
public sealed class ConfigurationWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly Dictionary<string, Action<string>> _registeredActions = new();
public ConfigurationWatcher()
{
_watcher = new FileSystemWatcher
{
Path = Path.Join(Utilities.OperatingDirectory, "Configuration"),
Filter = "*.json",
NotifyFilter = NotifyFilters.LastWrite
};
_watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
_watcher.Changed -= WatcherOnChanged;
_watcher.Dispose();
}
public void Register(string fileName, Action<string> fileUpdated)
{
if (_registeredActions.ContainsKey(fileName))
{
return;
}
_registeredActions.Add(fileName, fileUpdated);
}
public void Unregister(string fileName)
{
if (_registeredActions.ContainsKey(fileName))
{
_registeredActions.Remove(fileName);
}
}
private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs)
{
if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed ||
new FileInfo(eventArgs.FullPath).Length == 0)
{
return;
}
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath);
}
}

View File

@ -21,7 +21,7 @@ namespace IW4MAdmin.Application.IO
{ {
_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; _logger = logger;
} }
@ -69,7 +69,7 @@ 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)
{ {

View File

@ -28,7 +28,7 @@ namespace IW4MAdmin.Application.IO
_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>();

View File

@ -34,7 +34,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);

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

@ -17,76 +17,38 @@ 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.Extensions;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using IW4MAdmin.Application.Plugin;
using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Application.QueryHelpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Microsoft.Extensions.Hosting;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
using WebfrontCore.QueryHelpers.Models;
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 IServiceProvider _serviceProvider; private static ServiceProvider serviceProvider;
/// <summary> /// <summary>
/// entrypoint of the application /// entrypoint of the application
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25) public static async Task Main(string[] args)
{ {
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory); AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
var libraryName = eventArgs.Name.Split(",").First();
var overrides = new[] { nameof(SharedLibraryCore), nameof(Stats) };
if (!overrides.Contains(libraryName))
{
return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName == eventArgs.Name);
}
// added to be a bit more permissive with plugin references
return AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName?.StartsWith(libraryName) ?? false);
};
if (noConfirm)
{
AppContext.SetSwitch("NoConfirmPrompt", true);
}
Environment.SetEnvironmentVariable("MaxConcurrentRequests", (maxConcurrentRequests * Environment.ProcessorCount).ToString());
Environment.SetEnvironmentVariable("RequestQueueLimit", requestQueueLimit.ToString());
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");
@ -94,7 +56,7 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}"); Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("====================================================="); Console.WriteLine("=====================================================");
await LaunchAsync(); await LaunchAsync(args);
} }
/// <summary> /// <summary>
@ -105,14 +67,10 @@ 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();
if (ApplicationTask != null)
{ {
await _serverManager.Stop(); await ApplicationTask;
}
if (_applicationTask is not null)
{
await _applicationTask;
} }
} }
@ -120,47 +78,35 @@ namespace IW4MAdmin.Application
/// task that initializes application and starts the application monitoring and runtime tasks /// task that initializes application and starts the application monitoring and runtime tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task LaunchAsync() private static async Task LaunchAsync(string[] args)
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration()); var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger; Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version}", Version); IServiceCollection services = null;
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..."); logger.LogDebug("Configuring services...");
services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); await versionChecker.CheckVersion();
await configHandler.BuildAsync(); await ServerManager.Init();
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
await _serverManager.Init();
_applicationTask = RunApplicationTasksAsync(logger, _serverManager, _serviceProvider);
await _applicationTask;
logger.LogInformation("Shutdown completed successfully");
} }
catch (Exception e) catch (Exception e)
{ {
var failMessage = translationLookup == null string failMessage = translationLookup == null ? "Failed to initialize 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"); logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage); Console.WriteLine(failMessage);
@ -172,10 +118,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);
} }
@ -186,90 +134,62 @@ 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(logger, services);
await ApplicationTask;
}
catch (Exception e)
{
logger.LogCritical(e, "Failed to launch IW4MAdmin");
string failMessage = translationLookup == null ? "Failed to launch IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
if (ServerManager.IsRestartRequested)
{ {
goto restart; goto restart;
} }
serviceProvider.Dispose();
} }
/// <summary> /// <summary>
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static Task RunApplicationTasksAsync(ILogger logger, ApplicationManager applicationManager, private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
IServiceProvider serviceProvider)
{ {
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>(); var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>(); WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken) :
var masterCommunicator = serviceProvider.GetRequiredService<IMasterCommunication>(); Task.CompletedTask;
var webfrontLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
using var onWebfrontErrored = new ManualResetEventSlim();
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken).ContinueWith(continuation =>
{
if (!continuation.IsFaulted)
{
return;
}
logger.LogCritical("Unable to start webfront task. {Message}",
continuation.Exception?.InnerException?.Message);
logger.LogDebug(continuation.Exception, "Unable to start webfront task");
onWebfrontErrored.Set();
})
: Task.CompletedTask;
if (_serverManager.GetApplicationSettings().Configuration().EnableWebFront)
{
try
{
onWebfrontErrored.Wait(webfrontLifetime.ApplicationStarted);
}
catch
{
// ignored when webfront successfully starts
}
if (onWebfrontErrored.IsSet)
{
return Task.CompletedTask;
}
}
// 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(logger));
var inputThread = new Thread(ReadInput);
inputThread.Start(); inputThread.Start();
var tasks = new[] var tasks = new[]
{ {
applicationManager.Start(), ServerManager.Start(),
versionChecker.CheckVersion(),
webfrontTask, webfrontTask,
masterCommunicator.RunUploadStatus(_serverManager.CancellationToken), serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken)
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
}; };
logger.LogDebug("Starting webfront and input tasks"); logger.LogDebug("Starting webfront and input tasks");
return Task.WhenAll(tasks); await Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully");
Console.Write(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>
@ -282,46 +202,36 @@ namespace IW4MAdmin.Application
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, private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
@ -344,67 +254,40 @@ namespace IW4MAdmin.Application
// register the native commands // register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() 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)))
.Where(command => command.BaseType == typeof(Command)))
{ {
defaultLogger.LogDebug("Registered native command type {Name}", commandType.Name); defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
// register the plugin implementations // register the plugin implementations
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations(); var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins) foreach (var pluginType in pluginImplementations.Item1)
{ {
var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null; defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
try
{
var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
registrationMethod?.Invoke(null, new object[] { serviceCollection });
}
catch (Exception ex)
{
defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
}
} }
// register the plugin commands // register the plugin commands
foreach (var commandType in commands) foreach (var commandType in pluginImplementations.Item2)
{ {
defaultLogger.LogDebug("Registered plugin command type {Name}", commandType.FullName); defaultLogger.LogDebug("Registered plugin command type {name}", commandType.FullName);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
foreach (var configurationType in configurations) // register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{ {
defaultLogger.LogDebug("Registered plugin config type {Name}", configurationType.Name); serviceCollection.AddSingleton(scriptPlugin);
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);
}
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
foreach (var scriptPlugin in scriptPlugins)
{
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
sp.GetRequiredService<IScriptPluginFactory>()
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
} }
// register any eventable types // register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes() foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType)) .Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(plugins.SelectMany(asm => asm.Assembly.GetTypes()) .Union(pluginImplementations
.Distinct() .Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType)))) .Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{ {
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent; var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance); serviceCollection.AddSingleton(instance);
@ -417,62 +300,38 @@ namespace IW4MAdmin.Application
/// <summary> /// <summary>
/// Configures the dependency injection services /// Configures the dependency injection services
/// </summary> /// </summary>
private static void ConfigureServices(IServiceCollection serviceCollection) private static IServiceCollection ConfigureServices(string[] args)
{ {
// todo: this is a quick fix // setup the static resources (config/master api/translations)
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); var serviceCollection = new ServiceCollection();
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>()
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
// for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
if (appConfigHandler.Configuration()?.MasterUrl == new Uri("http://api.raidmax.org:5000"))
{
appConfigHandler.Configuration().MasterUrl = new ApplicationConfiguration().MasterUrl;
}
var appConfig = appConfigHandler.Configuration(); var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080") ? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl; : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var httpClient = new HttpClient var masterRestClient = RestClient.For<IMasterApi>(masterUri);
{ var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null) if (appConfig == null)
{ {
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate(); appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig); appConfigHandler.Set(appConfig);
appConfigHandler.Save().GetAwaiter().GetResult(); 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 // build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection serviceCollection
.AddBaseLogger(appConfig) .AddBaseLogger(appConfig)
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler) .AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler) .AddSingleton(
.AddSingleton(serviceProvider => new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>() IConfigurationHandler<CommandConfiguration>)
.AddSingleton(appConfig)
.AddSingleton(_serviceProvider =>
_serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration()) .Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>() .AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>() .AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
@ -485,10 +344,7 @@ 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<ClientService>()
.AddSingleton<PenaltyService>() .AddSingleton<PenaltyService>()
.AddSingleton<ChangeHistoryService>() .AddSingleton<ChangeHistoryService>()
@ -501,41 +357,25 @@ namespace IW4MAdmin.Application
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>() UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>() .AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>() .AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>() .AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>() .AddSingleton<IManager, ApplicationManager>()
#pragma warning disable CS0612
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>() .AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
#pragma warning restore CS0612
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>() .AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton<IClientStatisticCalculator, HitCalculator>()
.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<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
#pragma warning disable CS0618
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
#pragma warning restore CS0618
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>(); if (args.Contains("serialevents"))
serviceCollection.AddSource(); {
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient); serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
return serviceCollection;
} }
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig) private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)

View File

@ -1,9 +1,8 @@
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 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;

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,7 +5,6 @@ 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 Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,28 +15,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<MetaRegistration> 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 +33,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 +58,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 +75,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 +84,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.LogWarning("No client found with id {clientId} when generating profile meta", request.ClientId);
return metaList; return metaList;
} }
@ -118,7 +94,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 +105,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 +116,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 +125,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 +136,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,52 +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)
.OrderByDescending(change => change.TimeChanged);
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,17 +1,14 @@
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 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; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
@ -20,75 +17,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<ReceivedPenaltyResourceQueryHelper> 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); await 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 +67,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,7 +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 Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;

View File

@ -85,20 +85,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

@ -2,8 +2,9 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using IW4MAdmin.Plugins.Stats.Models;
using Data.Models.Client.Stats; using SharedLibraryCore.Database;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Migration namespace IW4MAdmin.Application.Migration
{ {

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,42 +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 _onSaving.WaitAsync(); var configContent = File.ReadAllText(FileName);
await using var fileStream = File.OpenRead(FileName); _configuration = JsonConvert.DeserializeObject<T>(configContent);
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
} }
catch (FileNotFoundException) catch (FileNotFoundException)
@ -62,39 +39,24 @@ 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
}; };
} }
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
} }
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);
await fileStream.DisposeAsync();
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
} }
public T Configuration() public T Configuration()

View File

@ -2,9 +2,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Data.Models;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
builder.Append(header); builder.Append(header);
builder.Append(config.NoticeLineSeparator); builder.Append(config.NoticeLineSeparator);
// build the reason // build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config)); var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
if (isNewLineSeparator) if (isNewLineSeparator)
{ {

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.Misc
{
public class EventLog : Dictionary<long, IList<GameEvent>>
{
private static JsonSerializerSettings serializationSettings;
public static JsonSerializerSettings BuildVcrSerializationSettings()
{
if (serializationSettings == null)
{
serializationSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
serializationSettings.Converters.Add(new IPAddressConverter());
serializationSettings.Converters.Add(new IPEndPointConverter());
serializationSettings.Converters.Add(new GameEventConverter());
serializationSettings.Converters.Add(new ClientEntityConverter());
}
return serializationSettings;
}
}
}

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);
country = reader.Country(address);
}
catch
{
// ignored
}
var response = new GeoLocationResult
{
Country = country?.Country.Name ?? "Unknown",
CountryCode = country?.Country.IsoCode ?? ""
};
return Task.FromResult((IGeoLocationResult)response);
}
}

View File

@ -1,171 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using InteractionRegistrationCallback =
System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken,
System.Threading.Tasks.Task<SharedLibraryCore.Interfaces.IInteractionData>>;
namespace IW4MAdmin.Application.Misc;
public class InteractionRegistration : IInteractionRegistration
{
private readonly ILogger<InteractionRegistration> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<string, InteractionRegistrationCallback> _interactions = new();
public InteractionRegistration(ILogger<InteractionRegistration> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
{
if (string.IsNullOrWhiteSpace(source))
{
throw new ArgumentException("Script interaction source cannot be null");
}
_logger.LogDebug("Registering script interaction {InteractionName} from {Source}", interactionName, source);
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);
if (plugin is not ScriptPlugin scriptPlugin)
{
return;
}
Task<IInteractionData> WrappedDelegate(int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, token, clientId, game, token));
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, WrappedDelegate);
}
else
{
_interactions[interactionName] = WrappedDelegate;
}
}
public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration)
{
if (!_interactions.ContainsKey(interactionName))
{
_logger.LogDebug("Registering interaction {InteractionName}", interactionName);
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_logger.LogDebug("Updating interaction {InteractionName}", interactionName);
_interactions[interactionName] = interactionRegistration;
}
}
public void UnregisterInteraction(string interactionName)
{
if (!_interactions.ContainsKey(interactionName))
{
return;
}
_logger.LogDebug("Unregistering interaction {InteractionName}", interactionName);
_interactions.TryRemove(interactionName, out _);
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null,
int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
}
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default)
{
if (!_interactions.ContainsKey(interactionId))
{
throw new ArgumentException($"Interaction with ID {interactionId} has not been registered");
}
try
{
var interaction = await _interactions[interactionId](targetId, game, token);
if (interaction.Action is not null)
{
return await interaction.Action(originId, targetId, game, meta, token);
}
if (interaction.ScriptAction is not null)
{
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>())
{
if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, originId, targetId, game, meta,
token);
}
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPluginV2>>())
{
if (plugin is not ScriptPluginV2 scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin
.QueryWithErrorHandling(interaction.ScriptAction, originId, targetId, game, meta, token)
?.ToString();
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for {InteractionName} and OriginId {ClientId}",
interactionId, originId);
}
return null;
}
private async Task<IEnumerable<IInteractionData>> GetInteractionsInternal(string prefix = null,
int? clientId = null, Reference.Game? game = null, CancellationToken token = default)
{
var interactions = _interactions
.Where(interaction => string.IsNullOrWhiteSpace(prefix) || interaction.Key.StartsWith(prefix)).Select(
async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for {InteractionName} and ClientId {ClientId}",
kvp.Key,
clientId);
return null;
}
});
return (await Task.WhenAll(interactions))
.Where(interaction => interaction is not null)
.ToList();
}
}

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

@ -26,8 +26,7 @@ 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 bool firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager) public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{ {
@ -94,24 +93,55 @@ 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.LogWarning(e, "Could not send heartbeat");
}
catch (AggregateException e)
{
_logger.LogWarning(e, "Could not send heartbeat");
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("Could not send heartbeat - {Message}", ex.Message); _logger.LogWarning(e, "Could not send heartbeat");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
} }
catch (Exception e)
{
_logger.LogWarning(e, "Could not send heartbeat");
}
try try
{ {
await Task.Delay(Interval, token); await Task.Delay(30000, token);
} }
catch catch
{ {
break; break;
@ -121,7 +151,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
{ {
@ -147,15 +177,14 @@ namespace IW4MAdmin.Application.Misc
Map = s.CurrentMap.Name, Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients, MaxClientNum = s.MaxClients,
Id = s.EndPoint, Id = s.EndPoint,
Port = (short)s.ListenPort, Port = (short)s.Port,
IPAddress = s.ListenAddress IPAddress = s.IP
}).ToList(), }).ToList()
WebfrontUrl = _appConfig.WebfrontUrl
}; };
Response<ResultMessage> response; Response<ResultMessage> response = null;
if (_firstHeartBeat) if (firstHeartBeat)
{ {
response = await _apiInstance.AddInstance(instance); response = await _apiInstance.AddInstance(instance);
} }
@ -163,12 +192,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.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent);
} }
} }
} }

View File

@ -7,10 +7,8 @@ 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 Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Models;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -18,7 +16,6 @@ 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;
@ -32,7 +29,7 @@ namespace IW4MAdmin.Application.Misc
_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)
@ -51,7 +48,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,121 +57,13 @@ 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); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
@ -188,34 +76,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 +96,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, IServiceProvider serviceProvider)
{
_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

@ -0,0 +1,141 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IPluginImporter
/// discovers plugins and script plugins
/// </summary>
public class PluginImporter : IPluginImporter
{
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private static readonly string PLUGIN_DIR = "Plugins";
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)
{
_logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
}
/// <summary>
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<IPlugin> DiscoverScriptPlugins()
{
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
if (Directory.Exists(pluginDir))
{
var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts());
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count());
if (scriptPluginFiles.Count() > 0)
{
foreach (string fileName in scriptPluginFiles)
{
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
var plugin = new ScriptPlugin(_logger, fileName);
yield return plugin;
}
}
}
}
/// <summary>
/// discovers all the C# assembly plugins and commands
/// </summary>
/// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>();
if (Directory.Exists(pluginDir))
{
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
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))
.Union(GetRemoteAssemblies())
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
}
}
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

@ -13,10 +13,10 @@ namespace IW4MAdmin.Application.Misc
{ {
public class RemoteAssemblyHandler : IRemoteAssemblyHandler public class RemoteAssemblyHandler : IRemoteAssemblyHandler
{ {
private const int KeyLength = 32; private const int keyLength = 32;
private const int TagLength = 16; private const int tagLength = 16;
private const int NonceLength = 12; private const int nonceLength = 12;
private const int IterationCount = 10000; private const int iterationCount = 10000;
private readonly ApplicationConfiguration _appconfig; private readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -30,7 +30,7 @@ namespace IW4MAdmin.Application.Misc
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies) public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
{ {
return DecryptContent(encryptedAssemblies) return DecryptContent(encryptedAssemblies)
.Select(Assembly.Load); .Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
} }
public IEnumerable<string> DecryptScripts(string[] encryptedScripts) public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
@ -38,24 +38,24 @@ namespace IW4MAdmin.Application.Misc
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript)); return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
} }
private IEnumerable<byte[]> DecryptContent(string[] content) private byte[][] DecryptContent(string[] content)
{ {
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId)) 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"); _logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return Array.Empty<byte[]>(); return new byte[0][];
} }
var assemblies = content.Select(piece => var assemblies = content.Select(piece =>
{ {
var byteContent = Convert.FromBase64String(piece); byte[] byteContent = Convert.FromBase64String(piece);
var encryptedContent = byteContent.Take(byteContent.Length - (TagLength + NonceLength)).ToArray(); byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
var tag = byteContent.Skip(byteContent.Length - (TagLength + NonceLength)).Take(TagLength).ToArray(); byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
var nonce = byteContent.Skip(byteContent.Length - NonceLength).Take(NonceLength).ToArray(); byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
var decryptedContent = new byte[encryptedContent.Length]; byte[] decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512); 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)); var encryption = new AesGcm(keyGen.GetBytes(keyLength));
try try
{ {

View File

@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc;
public class RemoteCommandService : IRemoteCommandService
{
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private readonly ClientService _clientService;
public RemoteCommandService(ILogger<RemoteCommandService> logger, ApplicationConfiguration appConfig, ClientService clientService)
{
_logger = logger;
_appConfig = appConfig;
_clientService = clientService;
}
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
return result;
}
public async Task<(bool, IEnumerable<CommandResponseInfo>)> ExecuteWithResult(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
if (originId < 1)
{
_logger.LogWarning("Not executing command {Command} for {Originid} because origin id is invalid", command,
originId);
return (false, Enumerable.Empty<CommandResponseInfo>());
}
var client = await _clientService.Get(originId);
client.CurrentServer = server;
command += $" {(targetId.HasValue ? $"@{targetId} " : "")}{string.Join(" ", arguments ?? Enumerable.Empty<string>())}";
var remoteEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Data = command.StartsWith(_appConfig.CommandPrefix) ||
command.StartsWith(_appConfig.BroadcastCommandPrefix)
? command
: $"{_appConfig.CommandPrefix}{command}",
Origin = client,
Owner = server,
IsRemote = true,
CorrelationId = Guid.NewGuid()
};
server.Manager.AddEvent(remoteEvent);
CommandResponseInfo[] response;
try
{
// wait for the event to process
var completedEvent =
await remoteEvent.WaitAsync(Utilities.DefaultCommandTimeout, server.Manager.CancellationToken);
if (completedEvent.FailReason == GameEvent.EventFailReason.Timeout)
{
response = new[]
{
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
}
};
}
else
{
response = completedEvent.Output.Select(output => new CommandResponseInfo()
{
Response = output,
ClientId = client.ClientId
}).ToArray();
}
}
catch (OperationCanceledException)
{
response = new[]
{
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
}
};
}
return (!remoteEvent.Failed, response);
}
}

View File

@ -1,32 +1,28 @@
using System; using SharedLibraryCore;
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.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin.Script namespace IW4MAdmin.Application.Misc
{ {
/// <summary> /// <summary>
/// generic script command implementation /// generic script command implementation
/// </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; 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, ILogger<ScriptCommand> logger)
IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
ITranslationLookup layout, ILogger<ScriptCommand> logger, IEnumerable<Reference.Game> supportedGames)
: base(config, layout) : base(config, layout)
{ {
_executeAction = executeAction; _executeAction = executeAction;
_logger = logger; _logger = logger;
Name = name; Name = name;
@ -34,8 +30,7 @@ namespace IW4MAdmin.Application.Plugin.Script
Description = description; Description = description;
RequiresTarget = isTargetRequired; RequiresTarget = isTargetRequired;
Permission = permission; Permission = permission;
Arguments = args.ToArray(); Arguments = args;
SupportedGames = supportedGames?.Select(game => (Server.Game)game).ToArray();
} }
public override async Task ExecuteAsync(GameEvent e) public override async Task ExecuteAsync(GameEvent e)
@ -47,11 +42,11 @@ namespace IW4MAdmin.Application.Plugin.Script
try try
{ {
await _executeAction(e); await Task.Run(() => _executeAction(e));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {Command} {@Event}", Name, e); _logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
} }
} }
} }

View File

@ -0,0 +1,364 @@
using System;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IPlugin
/// used to proxy script plugin requests
/// </summary>
public class ScriptPlugin : IPlugin
{
public string Name { get; set; }
public float Version { get; set; }
public string Author { get; set; }
/// <summary>
/// indicates if the plugin is a parser
/// </summary>
public bool IsParser { get; private set; }
public FileSystemWatcher Watcher { get; private set; }
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null)
{
_logger = logger;
_fileName = filename;
Watcher = new FileSystemWatcher()
{
Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory,
NotifyFilter = NotifyFilters.Size,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
Watcher.EnableRaisingEvents = true;
_onProcessing = new SemaphoreSlim(1, 1);
_registeredCommandNames = new List<string>();
}
~ScriptPlugin()
{
Watcher.Dispose();
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
{
await _onProcessing.WaitAsync();
try
{
// 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
// so if the new file is empty we can't process it yet
if (new FileInfo(_fileName).Length == 0L)
{
return;
}
bool firstRun = _scriptEngine == null;
// it's been loaded before so we need to call the unload event
if (!firstRun)
{
await OnUnloadAsync();
foreach (string commandName in _registeredCommandNames)
{
_logger.LogDebug("Removing plugin registered command {command}", commandName);
manager.RemoveCommandByName(commandName);
}
_registeredCommandNames.Clear();
}
successfullyLoaded = false;
string script;
using (var stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream, Encoding.Default))
{
script = await reader.ReadToEndAsync();
}
}
_scriptEngine = new Engine(cfg =>
cfg.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
})
.CatchClrExceptions());
try
{
_scriptEngine.Execute(script);
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} at {@locationInfo}",
nameof(Initialize), _fileName, ex.Location);
throw new PluginException($"A JavaScript parsing error occured while initializing script plugin");
}
catch (Exception e)
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(Initialize), _fileName);
throw new PluginException($"An unexpected error occured while initialization script plugin");
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author;
Name = pluginObject.name;
Version = (float)pluginObject.version;
var commands = _scriptEngine.GetValue("commands");
if (commands != JsValue.Undefined)
{
try
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
_logger.LogDebug("Adding plugin registered command {commandName}", command.Name);
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
}
catch (RuntimeBinderException e)
{
throw new PluginException($"Not all required fields were found: {e.Message}") { PluginFile = _fileName };
}
}
await OnLoadAsync(manager);
try
{
if (pluginObject.isParser)
{
IsParser = true;
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser);
}
}
catch (RuntimeBinderException) { }
if (!firstRun)
{
await OnLoadAsync(manager);
}
successfullyLoaded = true;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} initialization {@locationInfo}",
nameof(OnLoadAsync), _fileName, ex.Location);
throw new PluginException("An error occured while initializing script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(OnLoadAsync), _fileName);
throw new PluginException("An unexpected error occured while initializing script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public async Task OnEventAsync(GameEvent E, Server S)
{
if (successfullyLoaded)
{
await _onProcessing.WaitAsync();
try
{
_scriptEngine.SetValue("_gameEvent", E);
_scriptEngine.SetValue("_server", S);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", S.ToString()))
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} with event type {eventType} {@locationInfo}",
nameof(OnEventAsync), _fileName, E.Type, ex.Location);
}
throw new PluginException($"An error occured while executing action for script plugin");
}
catch (Exception e)
{
using (LogContext.PushProperty("Server", S.ToString()))
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin} with event type {eventType}",
nameof(OnEventAsync), _fileName, E.Type);
}
throw new PluginException($"An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
}
public Task OnLoadAsync(IManager manager)
{
_logger.LogDebug("OnLoad executing for {name}", Name);
_scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
}
public Task OnTickAsync(Server S)
{
_scriptEngine.SetValue("_server", S);
return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
}
public async Task OnUnloadAsync()
{
if (successfullyLoaded)
{
await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
}
}
/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
public IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
{
List<IManagerCommand> commandList = new List<IManagerCommand>();
// go through each defined command
foreach (var command in commands.AsArray())
{
dynamic dynamicCommand = command.ToObject();
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
string permission = dynamicCommand.permission;
bool targetRequired = false;
List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null;
try
{
arguments = dynamicCommand.arguments;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
try
{
targetRequired = dynamicCommand.targetRequired;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null)
{
foreach (var arg in dynamicCommand.arguments)
{
args.Add((arg.name, (bool)arg.required));
}
}
void execute(GameEvent e)
{
_scriptEngine.SetValue("_event", e);
var jsEventObject = _scriptEngine.GetValue("_event");
try
{
dynamicCommand.execute.Target.Invoke(jsEventObject);
}
catch (JavaScriptException ex)
{
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 };
}
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
}
return commandList;
}
}
}

View File

@ -1,8 +1,8 @@
using System; using SharedLibraryCore.Interfaces;
using System;
using System.Linq; using System.Linq;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script namespace IW4MAdmin.Application.Misc
{ {
/// <summary> /// <summary>
/// implementation of IScriptPluginServiceResolver /// implementation of IScriptPluginServiceResolver
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Plugin.Script
public object ResolveService(string serviceName, string[] genericParameters) public object ResolveService(string serviceName, string[] genericParameters)
{ {
var serviceType = DetermineRootType(serviceName, genericParameters.Length); var serviceType = DetermineRootType(serviceName, genericParameters.Length);
var genericTypes = genericParameters.Select(genericTypeParam => DetermineRootType(genericTypeParam)); var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray()); var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
return _serviceProvider.GetService(resolvedServiceType); return _serviceProvider.GetService(resolvedServiceType);
} }
@ -34,8 +34,8 @@ namespace IW4MAdmin.Application.Plugin.Script
{ {
var typeCollection = AppDomain.CurrentDomain.GetAssemblies() var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes()); .SelectMany(t => t.GetTypes());
var generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower(); string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(type => type.Name.ToLower() == generatedName); var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
if (serviceType == null) if (serviceType == null)
{ {

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,143 +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 SharedLibraryCore.Events.Management;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
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 bool _inProgress;
private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
IManagementEventSubscriptions.ClientStateDisposed += 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 async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
context.ConnectionHistory.Add(new EFClientConnectionHistory
{
ClientId = stateEvent.Client.ClientId,
ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
ConnectionType = stateEvent is ClientStateAuthorizeEvent
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
await context.SaveChangesAsync();
}
}
}

View File

@ -1,220 +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;
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, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
CancellationToken token = default)
{
_snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) =>
{
Reference.Game? game = null;
long? id = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
id = (long?)ids.Last();
}
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1);
int? maxClients;
DateTime? maxClientsTime;
if (id != null)
{
var clients = await snapshots.Where(snapshot => snapshot.ServerId == id)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
.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)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
.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), new object[] { gameCode, serverId }, _cacheTimeSpan, true);
try
{
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync),
new object[] { gameCode, serverId }, 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, Reference.Game? gameCode = null, CancellationToken token = default)
{
_serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) =>
{
Reference.Game? game = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
}
var count = await set.CountAsync(item => game == null || item.GameName == game,
cancellationToken);
var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod,
cancellationToken);
return (count, recentCount);
}, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true);
try
{
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, 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((set, ids, cancellationToken) =>
{
long? id = null;
if (ids.Any())
{
id = (long?)ids.First();
}
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == id)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { 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

@ -1,207 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin
{
/// <summary>
/// implementation of IPluginImporter
/// discovers plugins and script plugins
/// </summary>
public class PluginImporter : IPluginImporter
{
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private const string PluginDir = "Plugins";
private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init";
private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
private static readonly Type[] FilterTypes =
{
typeof(IPlugin),
typeof(IPluginV2),
typeof(Command),
typeof(IBaseConfiguration)
};
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi,
IRemoteAssemblyHandler remoteAssemblyHandler)
{
_logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
}
/// <summary>
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<(Type, string)> DiscoverScriptPlugins()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
if (!Directory.Exists(pluginDir))
{
return Enumerable.Empty<(Type, string)>();
}
var scriptPluginFiles =
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
var bothVersionPlugins = scriptPluginFiles.Select(fileName =>
{
_logger.LogDebug("Discovered script plugin {FileName}", fileName);
try
{
var fileContents = File.ReadAllLines(fileName);
var isValidV2 = fileContents.Any(line => Regex.IsMatch(line, PluginV2Match));
return isValidV2 ? (typeof(IPluginV2), fileName) : (typeof(IPlugin), fileName);
}
catch
{
return (typeof(IPlugin), fileName);
}
}).ToList();
return bothVersionPlugins;
}
/// <summary>
/// discovers all the C# assembly plugins and commands
/// </summary>
/// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
var pluginTypes = new List<Type>();
var commandTypes = new List<Type>();
var configurationTypes = new List<Type>();
if (!Directory.Exists(pluginDir))
{
return (pluginTypes, commandTypes, configurationTypes);
}
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
if (!dllFileNames.Any())
{
return (pluginTypes, commandTypes, configurationTypes);
}
// we only want to load the most recent assembly in case of duplicates
var assemblies = dllFileNames.Select(Assembly.LoadFrom)
.Union(GetRemoteAssemblies())
.GroupBy(assembly => assembly.FullName).Select(assembly =>
assembly.OrderByDescending(asm => asm.GetName().Version).First());
var eligibleAssemblyTypes = assemblies
.SelectMany(asm =>
{
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
}).Where(type =>
FilterTypes.Any(filterType => type.GetInterface(filterType.Name, false) != null) ||
(type.IsClass && FilterTypes.Contains(type.BaseType)));
foreach (var assemblyType in eligibleAssemblyTypes)
{
var isPlugin =
(assemblyType.GetInterface(nameof(IPlugin), false) ??
assemblyType.GetInterface(nameof(IPluginV2), false)) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isPlugin)
{
pluginTypes.Add(assemblyType);
continue;
}
var isCommand = assemblyType.IsClass && assemblyType.BaseType == typeof(Command) &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isCommand)
{
commandTypes.Add(assemblyType);
continue;
}
var isConfiguration = assemblyType.IsClass &&
assemblyType.GetInterface(nameof(IBaseConfiguration), false) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isConfiguration)
{
configurationTypes.Add(assemblyType);
}
}
_logger.LogDebug("Discovered {Count} plugin implementations", pluginTypes.Count);
_logger.LogDebug("Discovered {Count} plugin command implementations", commandTypes.Count);
_logger.LogDebug("Discovered {Count} plugin configuration implementations", configurationTypes.Count);
return (pluginTypes, commandTypes, configurationTypes);
}
private IEnumerable<Assembly> GetRemoteAssemblies()
{
try
{
_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
{
_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,567 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.CSharp.RuntimeBinder;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin.Script
{
/// <summary>
/// implementation of IPlugin
/// used to proxy script plugin requests
/// </summary>
public class ScriptPlugin : IPlugin
{
public string Name { get; set; }
public float Version { get; set; }
public string Author { get; set; }
/// <summary>
/// indicates if the plugin is a parser
/// </summary>
public bool IsParser { get; private set; }
public FileSystemWatcher Watcher { get; }
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1);
private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null)
{
_logger = logger;
_fileName = filename;
Watcher = new FileSystemWatcher
{
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
NotifyFilter = NotifyFilters.LastWrite,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
Watcher.EnableRaisingEvents = true;
_registeredCommandNames = new List<string>();
}
~ScriptPlugin()
{
Watcher.Dispose();
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
IScriptPluginServiceResolver serviceResolver, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
{
try
{
await _onProcessing.WaitAsync();
// 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
// so if the new file is empty we can't process it yet
if (new FileInfo(_fileName).Length == 0L)
{
return;
}
var firstRun = _scriptEngine == null;
// it's been loaded before so we need to call the unload event
if (!firstRun)
{
await OnUnloadAsync();
foreach (var commandName in _registeredCommandNames)
{
_logger.LogDebug("Removing plugin registered command {Command}", commandName);
manager.RemoveCommandByName(commandName);
}
_registeredCommandNames.Clear();
}
_successfullyLoaded = false;
string script;
await using (var stream =
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream, Encoding.Default))
{
script = await reader.ReadToEndAsync();
}
}
_scriptEngine?.Dispose();
_scriptEngine = new Engine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
typeof(ScriptPluginExtensions))
.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly,
typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
_scriptEngine.Execute(script);
if (!_scriptEngine.GetValue("init").IsUndefined())
{
// this is a v2 plugin and we don't want to try to load it
Watcher.EnableRaisingEvents = false;
Watcher.Dispose();
return;
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
Author = pluginObject.author;
Name = pluginObject.name;
Version = (float)pluginObject.version;
var commands = JsValue.Undefined;
try
{
commands = _scriptEngine.Evaluate("commands");
}
catch (JavaScriptException)
{
// ignore because commands aren't defined;
}
if (commands != JsValue.Undefined)
{
try
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
_logger.LogDebug("Adding plugin registered command {CommandName}", command.Name);
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
}
catch (RuntimeBinderException e)
{
throw new PluginException($"Not all required fields were found: {e.Message}")
{ PluginFile = _fileName };
}
}
async Task<bool> OnLoadTask()
{
await OnLoadAsync(manager);
return true;
}
var loadComplete = false;
try
{
if (pluginObject.isParser)
{
loadComplete = await OnLoadTask();
IsParser = true;
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser);
}
}
catch (RuntimeBinderException)
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine, configHandler);
if (!loadComplete)
{
_scriptEngine.SetValue("_configHandler", configWrapper);
loadComplete = await OnLoadTask();
}
}
if (!firstRun && !loadComplete)
{
loadComplete = await OnLoadTask();
}
_successfullyLoaded = loadComplete;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location, ex.JavaScriptStackTrace);
throw new PluginException("An error occured while initializing script plugin");
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), _fileName, jsEx.Location, jsEx.JavaScriptStackTrace);
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
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (!_successfullyLoaded)
{
return;
}
var shouldRelease = false;
try
{
await _onProcessing.WaitAsync(Utilities.DefaultCommandTimeout / 2);
shouldRelease = true;
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_gameEvent", gameEvent);
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
return _scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}, new { EventType = gameEvent.Type }, server);
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
public Task OnLoadAsync(IManager manager)
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_manager", manager);
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
});
return Task.CompletedTask;
}
public Task OnTickAsync(Server server)
{
return Task.CompletedTask;
}
public async Task OnUnloadAsync()
{
if (!_successfullyLoaded)
{
return;
}
try
{
await _onProcessing.WaitAsync();
_logger.LogDebug("OnUnload executing for {Name}", Name);
WrapJavaScriptErrorHandling(() => _scriptEngine.Evaluate("plugin.onUnloadAsync()"));
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public T ExecuteAction<T>(Delegate action, CancellationToken token, params object[] param)
{
var shouldRelease = false;
try
{
using var forceTimeout = new CancellationTokenSource(5000);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
_onProcessing.Wait(combined.Token);
shouldRelease = true;
_logger.LogDebug("Executing action for {Name}", Name);
return WrapJavaScriptErrorHandling(T() =>
{
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
},
new
{
Params = string.Join(", ",
param?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
Enumerable.Empty<string>())
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
public T WrapDelegate<T>(Delegate act, CancellationToken token, params object[] args)
{
var shouldRelease = false;
try
{
using var forceTimeout = new CancellationTokenSource(5000);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
_onProcessing.Wait(combined.Token);
shouldRelease = true;
_logger.LogDebug("Wrapping delegate action for {Name}", Name);
return WrapJavaScriptErrorHandling(
T() => (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)
?.ToObject(),
new
{
Params = string.Join(", ",
args?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
Enumerable.Empty<string>())
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
IScriptCommandFactory scriptCommandFactory)
{
var commandList = new List<IManagerCommand>();
// go through each defined command
foreach (var command in commands.AsArray())
{
dynamic dynamicCommand = command.ToObject();
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
if (dynamicCommand.permission is Data.Models.Client.EFClient.Permission perm)
{
dynamicCommand.permission = perm.ToString();
}
string permission = dynamicCommand.permission;
List<Reference.Game> supportedGames = null;
var targetRequired = false;
var args = new List<CommandArgument>();
dynamic arguments = null;
try
{
arguments = dynamicCommand.arguments;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
try
{
targetRequired = dynamicCommand.targetRequired;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null)
{
foreach (var arg in dynamicCommand.arguments)
{
args.Add(new CommandArgument { Name = arg.name, Required = (bool)arg.required });
}
}
try
{
foreach (var game in dynamicCommand.supportedGames)
{
supportedGames ??= new List<Reference.Game>();
supportedGames.Add(Enum.Parse(typeof(Reference.Game), game.ToString()));
}
}
catch (RuntimeBinderException)
{
// supported games is optional
}
async Task Execute(GameEvent gameEvent)
{
try
{
await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_event", gameEvent);
var jsEventObject = _scriptEngine.Evaluate("_event");
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
{
_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,
targetRequired, args, Execute, supportedGames));
}
return commandList;
}
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
[CallerMemberName] string methodName = "")
{
using (LogContext.PushProperty("Server", server?.ToString()))
{
try
{
return work();
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, Path.GetFileName(_fileName), ex.Location, ex.StackTrace, additionalData);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, _fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
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}",
methodName, Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
}
}
}
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,130 +0,0 @@
using System;
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 Jint.Native.Json;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginConfigurationWrapper
{
public event Action<JsValue, Delegate> ConfigurationUpdated;
private readonly ScriptPluginConfiguration _config;
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
private readonly Engine _scriptEngine;
private readonly JsonParser _engineParser;
private readonly List<(string, Delegate)> _updateCallbackActions = new();
private string _pluginName;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
{
_pluginName = pluginName;
_scriptEngine = scriptEngine;
_configHandler = configHandler;
_configHandler.Updated += OnConfigurationUpdated;
_config = configHandler.Get("ScriptPluginSettings", new ScriptPluginConfiguration()).GetAwaiter().GetResult();
_engineParser = new JsonParser(_scriptEngine);
}
~ScriptPluginConfigurationWrapper()
{
_configHandler.Updated -= OnConfigurationUpdated;
}
public void SetName(string name)
{
_pluginName = name;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double doubleValue)
{
castValue = AsInteger(doubleValue) ?? 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);
}
await _configHandler.Set(_config);
}
public JsValue GetValue(string key) => GetValue(key, null);
public JsValue GetValue(string key, Delegate updateCallback)
{
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>>();
}
if (updateCallback is not null)
{
_updateCallbackActions.Add((key, updateCallback));
}
try
{
return _engineParser.Parse(item!.ToString()!);
}
catch
{
// ignored
}
return JsValue.FromObject(_scriptEngine, item);
}
private static int? AsInteger(double value)
{
return int.TryParse(value.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
}
private void OnConfigurationUpdated(ScriptPluginConfiguration config)
{
foreach (var callback in _updateCallbackActions)
{
ConfigurationUpdated?.Invoke(GetValue(callback.Item1), callback.Item2);
}
}
}

View File

@ -1,32 +0,0 @@
using System;
using IW4MAdmin.Application.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginFactory : IScriptPluginFactory
{
private readonly IServiceProvider _serviceProvider;
public ScriptPluginFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object CreateScriptPlugin(Type type, string fileName)
{
if (type == typeof(IPlugin))
{
return new ScriptPlugin(_serviceProvider.GetRequiredService<ILogger<ScriptPlugin>>(),
fileName);
}
return new ScriptPluginV2(fileName, _serviceProvider.GetRequiredService<ILogger<ScriptPluginV2>>(),
_serviceProvider.GetRequiredService<IScriptPluginServiceResolver>(),
_serviceProvider.GetRequiredService<IScriptCommandFactory>(),
_serviceProvider.GetRequiredService<IConfigurationHandlerV2<ScriptPluginConfiguration>>(),
_serviceProvider.GetRequiredService<IInteractionRegistration>());
}
}

View File

@ -1,143 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jint.Native;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginHelper
{
private readonly IManager _manager;
private readonly ScriptPluginV2 _scriptPlugin;
private readonly SemaphoreSlim _onRequestRunning = new(1, 1);
private const int RequestTimeout = 5000;
public ScriptPluginHelper(IManager manager, ScriptPluginV2 scriptPlugin)
{
_manager = manager;
_scriptPlugin = scriptPlugin;
}
public void GetUrl(string url, Delegate callback)
{
RequestUrl(new ScriptPluginWebRequest(url), callback);
}
public void GetUrl(string url, string bearerToken, Delegate callback)
{
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
RequestUrl(new ScriptPluginWebRequest(url, Headers: headers), callback);
}
public void PostUrl(string url, string body, string bearerToken, Delegate callback)
{
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
RequestUrl(
new ScriptPluginWebRequest(url, body, "POST", Headers: headers), callback);
}
public void RequestUrl(ScriptPluginWebRequest request, Delegate callback)
{
Task.Run(() =>
{
try
{
var response = RequestInternal(request);
_scriptPlugin.ExecuteWithErrorHandling(scriptEngine =>
{
callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.FromObject(scriptEngine, response) });
});
}
catch
{
// ignored
}
});
}
public void RequestNotifyAfterDelay(int delayMs, Delegate callback)
{
Task.Run(async () =>
{
try
{
await Task.Delay(delayMs, _manager.CancellationToken);
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }));
}
catch
{
// ignored
}
});
}
public void RegisterDynamicCommand(JsValue command)
{
_scriptPlugin.RegisterDynamicCommand(command.ToObject());
}
private object RequestInternal(ScriptPluginWebRequest request)
{
var entered = false;
using var tokenSource = new CancellationTokenSource(RequestTimeout);
using var client = new HttpClient();
try
{
_onRequestRunning.Wait(tokenSource.Token);
entered = true;
var requestMessage = new HttpRequestMessage(new HttpMethod(request.Method), request.Url);
if (request.Body is not null)
{
requestMessage.Content = new StringContent(request.Body.ToString() ?? string.Empty, Encoding.Default,
request.ContentType ?? "text/plain");
}
if (request.Headers is not null)
{
foreach (var (key, value) in request.Headers)
{
if (!string.IsNullOrWhiteSpace(key))
{
requestMessage.Headers.Add(key, value);
}
}
}
var response = client.Send(requestMessage, tokenSource.Token);
using var reader = new StreamReader(response.Content.ReadAsStream());
return reader.ReadToEnd();
}
catch (HttpRequestException ex)
{
return new
{
ex.StatusCode,
ex.Message,
IsError = true
};
}
catch (Exception ex)
{
return new
{
ex.Message,
IsError = true
};
}
finally
{
if (entered)
{
_onRequestRunning.Release(1);
}
}
}
}

View File

@ -1,201 +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.Plugin.Script;
[Obsolete("This architecture is superseded by the request notify delay architecture")]
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
private Timer _timer;
private Action _actions;
private Delegate _jsAction;
private string _actionName;
private int _interval = DefaultInterval;
private long _waitingCount;
private const int DefaultDelay = 0;
private const int DefaultInterval = 1000;
private const int MaxWaiting = 10;
private readonly ILogger _logger;
private readonly SemaphoreSlim _onRunningTick = new(1, 1);
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...");
_timer ??= new Timer(callback => _actions(), null, delay, interval);
_interval = 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 = OnTickInternal;
}
private void ReleaseThreads(bool releaseOnRunning, bool releaseOnDependent)
{
if (releaseOnRunning && _onRunningTick.CurrentCount == 0)
{
_logger.LogDebug("-Releasing OnRunning for timer");
_onRunningTick.Release(1);
}
if (releaseOnDependent && _onDependentAction?.CurrentCount == 0)
{
_onDependentAction?.Release(1);
}
}
private async void OnTickInternal()
{
var releaseOnRunning = false;
var releaseOnDependent = false;
try
{
try
{
if (Interlocked.Read(ref _waitingCount) > MaxWaiting)
{
_logger.LogWarning("Reached max number of waiting count ({WaitingCount}) for {OnTick}",
_waitingCount, nameof(OnTickInternal));
return;
}
Interlocked.Increment(ref _waitingCount);
using var tokenSource1 = new CancellationTokenSource();
tokenSource1.CancelAfter(TimeSpan.FromMilliseconds(_interval));
await _onRunningTick.WaitAsync(tokenSource1.Token);
releaseOnRunning = true;
}
catch (OperationCanceledException)
{
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
nameof(OnTickInternal));
return;
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
try
{
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
if (_onDependentAction is not null)
{
await _onDependentAction.WaitAsync(tokenSource.Token);
releaseOnDependent = true;
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("Dependent action did not release in allotted time so we are cancelling this tick");
return;
}
_logger.LogDebug("+Running OnTick for timer");
var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
{
_logger.LogError(jsx,
"Could not execute timer tick for script action {ActionName} [{@LocationInfo}] [{@StackTrace}]",
_actionName,
jsx.Location, jsx.JavaScriptStackTrace);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
}
finally
{
ReleaseThreads(releaseOnRunning, releaseOnDependent);
Interlocked.Decrement(ref _waitingCount);
}
}
public void SetDependency(SemaphoreSlim dependentSemaphore)
{
_onDependentAction = dependentSemaphore;
}
public bool IsRunning { get; private set; }
}

View File

@ -1,597 +0,0 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Extensions;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using JavascriptEngine = Jint.Engine;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginV2 : IPluginV2
{
public string Name { get; private set; } = string.Empty;
public string Author { get; private set; } = string.Empty;
public string Version { get; private set; }
private readonly string _fileName;
private readonly ILogger<ScriptPluginV2> _logger;
private readonly IScriptPluginServiceResolver _pluginServiceResolver;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
private readonly IInteractionRegistration _interactionRegistration;
private readonly SemaphoreSlim _onProcessingScript = new(1, 1);
private readonly SemaphoreSlim _onLoadingFile = new(1, 1);
private readonly FileSystemWatcher _scriptWatcher;
private readonly List<string> _registeredCommandNames = new();
private readonly List<string> _registeredInteractions = new();
private readonly Dictionary<MethodInfo, List<object>> _registeredEvents = new();
private IManager _manager;
private bool _firstInitialization = true;
private record ScriptPluginDetails(string Name, string Author, string Version,
ScriptPluginCommandDetails[] Commands, ScriptPluginInteractionDetails[] Interactions);
private record ScriptPluginCommandDetails(string Name, string Description, string Alias, string Permission,
bool TargetRequired, CommandArgument[] Arguments, IEnumerable<Reference.Game> SupportedGames, Delegate Execute);
private JavascriptEngine ScriptEngine
{
get
{
lock (ActiveEngines)
{
return ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
}
}
}
private record ScriptPluginInteractionDetails(string Name, Delegate Action);
private ScriptPluginConfigurationWrapper _scriptPluginConfigurationWrapper;
private int _nextEngineId;
private static readonly Dictionary<string, JavascriptEngine> ActiveEngines = new();
public ScriptPluginV2(string fileName, ILogger<ScriptPluginV2> logger,
IScriptPluginServiceResolver pluginServiceResolver, IScriptCommandFactory scriptCommandFactory,
IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler,
IInteractionRegistration interactionRegistration)
{
_fileName = fileName;
_logger = logger;
_pluginServiceResolver = pluginServiceResolver;
_scriptCommandFactory = scriptCommandFactory;
_configHandler = configHandler;
_interactionRegistration = interactionRegistration;
_scriptWatcher = new FileSystemWatcher
{
Path = Path.Join(Utilities.OperatingDirectory, "Plugins"),
NotifyFilter = NotifyFilters.LastWrite,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
IManagementEventSubscriptions.Load += OnLoad;
}
public void ExecuteWithErrorHandling(Action<Engine> work)
{
WrapJavaScriptErrorHandling(() =>
{
work(ScriptEngine);
return true;
}, _logger, _fileName, _onProcessingScript);
}
public object QueryWithErrorHandling(Delegate action, params object[] args)
{
return WrapJavaScriptErrorHandling(() =>
{
var jsArgs = args?.Select(param => JsValue.FromObject(ScriptEngine, param)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, jsArgs);
return result;
}, _logger, _fileName, _onProcessingScript);
}
public void RegisterDynamicCommand(object command)
{
var parsedCommand = ParseScriptCommandDetails(command);
RegisterCommand(_manager, parsedCommand.First());
}
private async Task OnLoad(IManager manager, CancellationToken token)
{
_manager = manager;
var entered = false;
try
{
await _onLoadingFile.WaitAsync(token);
entered = true;
_logger.LogDebug("{Method} executing for {Plugin}", nameof(OnLoad), _fileName);
if (new FileInfo(_fileName).Length == 0L)
{
return;
}
_scriptWatcher.EnableRaisingEvents = false;
UnregisterScriptEntities(manager);
ResetEngineState();
if (_firstInitialization)
{
_scriptWatcher.Changed += async (_, _) => await OnLoad(manager, token);
_firstInitialization = false;
}
await using var stream =
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream, Encoding.Default);
var pluginScript = await reader.ReadToEndAsync();
var pluginDetails = WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
ScriptEngine.Execute(pluginScript);
#pragma warning disable CS8974
var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper),
JsValue.FromObject(ScriptEngine, _pluginServiceResolver),
JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper),
JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this)));
#pragma warning restore CS8974
if (initResult.IsNull() || initResult.IsUndefined())
{
return null;
}
return AsScriptPluginInstance(initResult.ToObject());
}, _logger, _fileName, _onProcessingScript);
if (pluginDetails is null)
{
_logger.LogInformation("No valid script plugin signature found for {FilePath}", _fileName);
return;
}
foreach (var command in pluginDetails.Commands)
{
RegisterCommand(manager, command);
_logger.LogDebug("Registered script plugin command {Command} for {Plugin}", command.Name,
pluginDetails.Name);
}
foreach (var interaction in pluginDetails.Interactions)
{
RegisterInteraction(interaction);
_logger.LogDebug("Registered script plugin interaction {Interaction} for {Plugin}", interaction.Name,
pluginDetails.Name);
}
Name = pluginDetails.Name;
Author = pluginDetails.Author;
Version = pluginDetails.Version;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error encountered loading script plugin {Name}", _fileName);
}
finally
{
if (entered)
{
_onLoadingFile.Release(1);
_scriptWatcher.EnableRaisingEvents = true;
}
_logger.LogDebug("{Method} completed for {Plugin}", nameof(OnLoad), _fileName);
}
}
private void RegisterInteraction(ScriptPluginInteractionDetails interaction)
{
Task<IInteractionData> Action(int? targetId, Reference.Game? game, CancellationToken token) =>
WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
var args = new object[] { targetId, game, token }.Select(arg => JsValue.FromObject(ScriptEngine, arg))
.ToArray();
if (interaction.Action.DynamicInvoke(JsValue.Undefined, args) is not ObjectWrapper result)
{
throw new PluginException("Invalid interaction object returned");
}
return Task.FromResult((IInteractionData)result.ToObject());
}, _logger, _fileName, _onProcessingScript);
_interactionRegistration.RegisterInteraction(interaction.Name, Action);
_registeredInteractions.Add(interaction.Name);
}
private void RegisterCommand(IManager manager, ScriptPluginCommandDetails command)
{
Task Execute(GameEvent gameEvent) =>
WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
command.Execute.DynamicInvoke(JsValue.Undefined,
new[] { JsValue.FromObject(ScriptEngine, gameEvent) });
return Task.CompletedTask;
}, _logger, _fileName, _onProcessingScript);
var scriptCommand = _scriptCommandFactory.CreateScriptCommand(command.Name, command.Alias,
command.Description,
command.Permission, command.TargetRequired,
command.Arguments, Execute, command.SupportedGames);
manager.RemoveCommandByName(scriptCommand.Name);
manager.AddAdditionalCommand(scriptCommand);
if (!_registeredCommandNames.Contains(scriptCommand.Name))
{
_registeredCommandNames.Add(scriptCommand.Name);
}
}
private void ResetEngineState()
{
JavascriptEngine oldEngine = null;
lock (ActiveEngines)
{
if (ActiveEngines.ContainsKey($"{GetHashCode()}-{_nextEngineId}"))
{
oldEngine = ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
_logger.LogDebug("Removing script engine from active list {HashCode}", _nextEngineId);
ActiveEngines.Remove($"{GetHashCode()}-{_nextEngineId}");
}
}
Interlocked.Increment(ref _nextEngineId);
oldEngine?.Dispose();
var newEngine = new JavascriptEngine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
typeof(ScriptPluginExtensions), typeof(LoggerExtensions))
.AllowClr(typeof(System.Net.Http.HttpClient).Assembly, typeof(EFClient).Assembly,
typeof(Utilities).Assembly, typeof(Encoding).Assembly, typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly, typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly, typeof(ScriptPluginWebRequest).Assembly)
.CatchClrExceptions()
.AddObjectConverter(new EnumsToStringConverter()));
lock (ActiveEngines)
{
_logger.LogDebug("Adding script engine to active list {HashCode}", _nextEngineId);
ActiveEngines.Add($"{GetHashCode()}-{_nextEngineId}", newEngine);
}
_scriptPluginConfigurationWrapper =
new ScriptPluginConfigurationWrapper(_fileName.Split(Path.DirectorySeparatorChar).Last(), ScriptEngine,
_configHandler);
_scriptPluginConfigurationWrapper.ConfigurationUpdated += (configValue, callbackAction) =>
{
WrapJavaScriptErrorHandling(() =>
{
callbackAction.DynamicInvoke(JsValue.Undefined, new[] { configValue });
return Task.CompletedTask;
}, _logger, _fileName, _onProcessingScript);
};
}
private void UnregisterScriptEntities(IManager manager)
{
foreach (var commandName in _registeredCommandNames)
{
manager.RemoveCommandByName(commandName);
_logger.LogDebug("Unregistered script plugin command {Command} for {Plugin}", commandName, Name);
}
_registeredCommandNames.Clear();
foreach (var interactionName in _registeredInteractions)
{
_interactionRegistration.UnregisterInteraction(interactionName);
}
_registeredInteractions.Clear();
foreach (var (removeMethod, subscriptions) in _registeredEvents)
{
foreach (var subscription in subscriptions)
{
removeMethod.Invoke(null, new[] { subscription });
}
subscriptions.Clear();
}
_registeredEvents.Clear();
}
private void EventCallbackWrapper(string eventCallbackName, Delegate javascriptAction)
{
var eventCategory = eventCallbackName.Split(".")[0];
var eventCategoryType = eventCategory switch
{
nameof(IManagementEventSubscriptions) => typeof(IManagementEventSubscriptions),
nameof(IGameEventSubscriptions) => typeof(IGameEventSubscriptions),
nameof(IGameServerEventSubscriptions) => typeof(IGameServerEventSubscriptions),
_ => null
};
if (eventCategoryType is null)
{
_logger.LogWarning("{EventCategory} is not a valid subscription category", eventCategory);
return;
}
var eventName = eventCallbackName.Split(".")[1];
var eventAddMethod = eventCategoryType.GetMethods()
.FirstOrDefault(method => method.Name.StartsWith($"add_{eventName}"));
var eventRemoveMethod = eventCategoryType.GetMethods()
.FirstOrDefault(method => method.Name.StartsWith($"remove_{eventName}"));
if (eventAddMethod is null || eventRemoveMethod is null)
{
_logger.LogWarning("{EventName} is not a valid subscription event", eventName);
return;
}
var genericType = eventAddMethod.GetParameters()[0].ParameterType.GetGenericArguments()[0];
var eventWrapper =
typeof(ScriptPluginV2).GetMethod(nameof(BuildEventWrapper), BindingFlags.Static | BindingFlags.NonPublic)!
.MakeGenericMethod(genericType)
.Invoke(null,
new object[]
{ _logger, _fileName, javascriptAction, GetHashCode(), _nextEngineId, _onProcessingScript });
eventAddMethod.Invoke(null, new[] { eventWrapper });
if (!_registeredEvents.ContainsKey(eventRemoveMethod))
{
_registeredEvents.Add(eventRemoveMethod, new List<object> { eventWrapper });
}
else
{
_registeredEvents[eventRemoveMethod].Add(eventWrapper);
}
}
private static Func<TEventType, CancellationToken, Task> BuildEventWrapper<TEventType>(ILogger logger,
string fileName, Delegate javascriptAction, int hashCode, int engineId, SemaphoreSlim onProcessingScript)
{
return (coreEvent, token) =>
{
return WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(hashCode, engineId))
{
return Task.CompletedTask;
}
JavascriptEngine engine;
lock (ActiveEngines)
{
engine = ActiveEngines[$"{hashCode}-{engineId}"];
}
var args = new object[] { coreEvent, token }
.Select(param => JsValue.FromObject(engine, param))
.ToArray();
javascriptAction.DynamicInvoke(JsValue.Undefined, args);
return Task.CompletedTask;
}, logger, fileName, onProcessingScript, (coreEvent as GameServerEvent)?.Server,
additionalData: coreEvent.GetType().Name);
};
}
private static bool IsEngineDisposed(int hashCode, int engineId)
{
lock (ActiveEngines)
{
return !ActiveEngines.ContainsKey($"{hashCode}-{engineId}");
}
}
private static TResultType WrapJavaScriptErrorHandling<TResultType>(Func<TResultType> work, ILogger logger,
string fileName, SemaphoreSlim onProcessingScript, IGameServer server = null, object additionalData = null,
bool throwException = false,
[CallerMemberName] string methodName = "")
{
using (LogContext.PushProperty("Server", server?.Id))
{
var waitCompleted = false;
try
{
onProcessingScript.Wait();
waitCompleted = true;
return work();
}
catch (JavaScriptException ex)
{
logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, Path.GetFileName(fileName), ex.Location, ex.StackTrace, additionalData);
if (throwException)
{
throw new PluginException("A runtime error occured while executing action for script plugin");
}
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
if (throwException)
{
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}",
methodName, Path.GetFileName(fileName));
if (throwException)
{
throw new PluginException("An error occured while executing action for script plugin");
}
}
finally
{
if (waitCompleted)
{
onProcessingScript.Release(1);
}
}
}
return default;
}
private static ScriptPluginDetails AsScriptPluginInstance(dynamic source)
{
var commandDetails = ParseScriptCommandDetails(source);
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
if (HasProperty(source, "interactions") && source.interactions is dynamic[])
{
interactionDetails = ((dynamic[])source.interactions).Select(interaction =>
{
var name = HasProperty(interaction, "name") && interaction.name is string
? (string)interaction.name
: string.Empty;
var action = HasProperty(interaction, "action") && interaction.action is Delegate
? (Delegate)interaction.action
: null;
return new ScriptPluginInteractionDetails(name, action);
}).ToArray();
}
var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty;
var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty;
var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty;
return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails);
}
private static ScriptPluginCommandDetails[] ParseScriptCommandDetails(dynamic source)
{
var commandDetails = Array.Empty<ScriptPluginCommandDetails>();
if (HasProperty(source, "commands") && source.commands is dynamic[])
{
commandDetails = ((dynamic[])source.commands).Select(command =>
{
var commandArgs = Array.Empty<CommandArgument>();
if (HasProperty(command, "arguments") && command.arguments is dynamic[])
{
commandArgs = ((dynamic[])command.arguments).Select(argument => new CommandArgument
{
Name = HasProperty(argument, "name") ? argument.name : string.Empty,
Required = HasProperty(argument, "required") && argument.required is bool &&
(bool)argument.required
}).ToArray();
}
var name = HasProperty(command, "name") && command.name is string
? (string)command.name
: string.Empty;
var description = HasProperty(command, "description") && command.description is string
? (string)command.description
: string.Empty;
var alias = HasProperty(command, "alias") && command.alias is string
? (string)command.alias
: string.Empty;
var permission = HasProperty(command, "permission") && command.permission is string
? (string)command.permission
: string.Empty;
var isTargetRequired = HasProperty(command, "targetRequired") && command.targetRequired is bool &&
(bool)command.targetRequired;
var supportedGames =
HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable<object>
? ((IEnumerable<object>)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString()))
.Select(game =>
Enum.Parse<Reference.Game>(game.ToString()!))
: Array.Empty<Reference.Game>();
var execute = HasProperty(command, "execute") && command.execute is Delegate
? (Delegate)command.execute
: (GameEvent _) => Task.CompletedTask;
return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired,
commandArgs, supportedGames, execute);
}).ToArray();
}
return commandDetails;
}
private static bool HasProperty(dynamic source, string name)
{
Type objType = source.GetType();
if (objType == typeof(ExpandoObject))
{
return ((IDictionary<string, object>)source).ContainsKey(name);
}
return objType.GetProperty(name) != null;
}
public class EnumsToStringConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is Enum)
{
result = value.ToString();
return true;
}
result = JsValue.Null;
return false;
}
}
}

View File

@ -1,6 +0,0 @@
using System.Collections.Generic;
namespace IW4MAdmin.Application.Plugin.Script;
public record ScriptPluginWebRequest(string Url, object Body = null, string Method = "GET", string ContentType = "text/plain",
Dictionary<string, string> Headers = null);

View File

@ -1,297 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.QueryHelpers.Models;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.QueryHelpers;
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
{
public ApplicationConfiguration _appConfig { get; }
private readonly IDatabaseContextFactory _contextFactory;
private readonly IGeoLocationService _geoLocationService;
private class ClientAlias
{
public EFClient Client { get; set; }
public EFAlias Alias { get; set; }
}
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService,
ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_contextFactory = contextFactory;
_geoLocationService = geoLocationService;
}
public async Task<ResourceQueryHelperResult<ClientResourceResponse>> QueryResource(ClientResourceRequest query)
{
await using var context = _contextFactory.CreateContext(false);
var iqAliases = context.Aliases.AsQueryable();
var iqClients = context.Clients.AsQueryable();
var iqClientAliases = iqClients.Join(iqAliases, client => client.AliasLinkId, alias => alias.LinkId,
(client, alias) => new ClientAlias { Client = client, Alias = alias });
return await StartFromClient(query, iqClientAliases, iqClients);
}
private async Task<ResourceQueryHelperResult<ClientResourceResponse>> StartFromClient(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases, IQueryable<EFClient> iqClients)
{
if (!string.IsNullOrWhiteSpace(query.ClientGuid))
{
clientAliases = SearchByGuid(query, clientAliases);
}
if (query.ClientLevel is not null)
{
clientAliases = SearchByLevel(query, clientAliases);
}
if (query.ClientConnected is not null)
{
clientAliases = SearchByLastConnection(query, clientAliases);
}
if (query.GameName is not null)
{
clientAliases = SearchByGame(query, clientAliases);
}
if (!string.IsNullOrWhiteSpace(query.ClientName))
{
clientAliases = SearchByName(query, clientAliases);
}
if (!string.IsNullOrWhiteSpace(query.ClientIp))
{
clientAliases = SearchByIp(query, clientAliases,
_appConfig.HasPermission(query.RequesterPermission, WebfrontEntity.ClientIPAddress,
WebfrontPermission.Read));
}
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
iqGroupedClientAliases = query.Direction == SortDirection.Descending
? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection)
: iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection);
var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId)
.Skip(query.Offset)
.Take(query.Count);
// this pulls in more records than we need, but it's more efficient than ordering grouped entities
var clientLookups = await clientAliases
.Where(clientAlias => clientIds.Contains(clientAlias.Client.ClientId))
.Select(clientAlias => new ClientResourceResponse
{
ClientId = clientAlias.Client.ClientId,
AliasId = clientAlias.Alias.AliasId,
LinkId = clientAlias.Client.AliasLinkId,
CurrentClientName = clientAlias.Client.CurrentAlias.Name,
MatchedClientName = clientAlias.Alias.Name,
CurrentClientIp = clientAlias.Client.CurrentAlias.IPAddress,
MatchedClientIp = clientAlias.Alias.IPAddress,
ClientLevel = clientAlias.Client.Level.ToLocalizedLevelName(),
ClientLevelValue = clientAlias.Client.Level,
LastConnection = clientAlias.Client.LastConnection,
Game = clientAlias.Client.GameName
})
.ToListAsync();
var groupClients = clientLookups.GroupBy(x => x.ClientId);
var orderedClients = query.Direction == SortDirection.Descending
? groupClients.OrderByDescending(SearchByAliasLocal(query.ClientName, query.ClientIp))
: groupClients.OrderBy(SearchByAliasLocal(query.ClientName, query.ClientIp));
var clients = orderedClients.Select(client => client.First()).ToList();
await ProcessAliases(query, clients);
return new ResourceQueryHelperResult<ClientResourceResponse>
{
Results = clients
};
}
private async Task ProcessAliases(ClientResourceRequest query, IEnumerable<ClientResourceResponse> clients)
{
await Parallel.ForEachAsync(clients, new ParallelOptions { MaxDegreeOfParallelism = 15 },
async (client, token) =>
{
if (!query.IncludeGeolocationData || client.CurrentClientIp is null)
{
return;
}
var geolocationData = await _geoLocationService.Locate(client.CurrentClientIp.ConvertIPtoString());
client.ClientCountryCode = geolocationData.CountryCode;
if (!string.IsNullOrWhiteSpace(client.ClientCountryCode))
{
client.ClientCountryDisplayName = geolocationData.Country;
}
});
}
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
string ipAddress)
{
return group =>
{
ClientResourceResponse match = null;
var lowercaseClientName = clientName?.ToLower();
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
{
match = group.ToList().FirstOrDefault(SearchByNameLocal(lowercaseClientName));
}
if (match is null && !string.IsNullOrWhiteSpace(ipAddress))
{
match = group.ToList().FirstOrDefault(SearchByIpLocal(ipAddress));
}
return (match ?? group.First()).LastConnection;
};
}
private static Func<ClientResourceResponse, bool> SearchByNameLocal(string clientName)
{
return clientResourceResponse =>
clientResourceResponse.MatchedClientName.Contains(clientName);
}
private static Func<ClientResourceResponse, bool> SearchByIpLocal(string clientIp)
{
return clientResourceResponse => clientResourceResponse.MatchedClientIp.ConvertIPtoString().Contains(clientIp);
}
private static IQueryable<ClientAlias> SearchByName(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases)
{
var lowerCaseQueryName = query.ClientName.ToLower();
clientAliases = clientAliases.Where(query.IsExactClientName
? ExactNameMatch(lowerCaseQueryName)
: LikeNameMatch(lowerCaseQueryName));
return clientAliases;
}
private static Expression<Func<ClientAlias, bool>> LikeNameMatch(string lowerCaseQueryName)
{
return clientAlias => EF.Functions.Like(
clientAlias.Alias.SearchableName,
$"%{lowerCaseQueryName}%") || EF.Functions.Like(
clientAlias.Alias.Name.ToLower(),
$"%{lowerCaseQueryName}%");
}
private static Expression<Func<ClientAlias, bool>> ExactNameMatch(string lowerCaseQueryName)
{
return clientAlias =>
lowerCaseQueryName == clientAlias.Alias.Name || lowerCaseQueryName == clientAlias.Alias.SearchableName;
}
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases, bool canSearchIP)
{
var ipString = query.ClientIp.Trim();
var ipAddress = ipString.ConvertToIP();
if (ipAddress != null && ipString.Split('.').Length == 4 && query.IsExactClientIp)
{
clientAliases = clientAliases.Where(clientAlias =>
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
}
else if(canSearchIP)
{
clientAliases = clientAliases.Where(clientAlias =>
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));
}
return clientAliases;
}
private static IQueryable<ClientAlias> SearchByGuid(ClientResourceRequest query,
IQueryable<ClientAlias> clients)
{
var guidString = query.ClientGuid.Trim();
var parsedGuids = new List<long>();
long guid = 0;
try
{
guid = guidString.ConvertGuidToLong(NumberStyles.HexNumber, false, 0);
}
catch
{
// ignored
}
if (guid != 0)
{
parsedGuids.Add(guid);
}
try
{
guid = guidString.ConvertGuidToLong(NumberStyles.Integer, false, 0);
}
catch
{
// ignored
}
if (guid != 0)
{
parsedGuids.Add(guid);
}
if (!parsedGuids.Any())
{
return clients;
}
clients = clients.Where(client => parsedGuids.Contains(client.Client.NetworkId));
return clients;
}
private static IQueryable<ClientAlias> SearchByLevel(ClientResourceRequest query, IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.Level == query.ClientLevel);
return clients;
}
private static IQueryable<ClientAlias> SearchByLastConnection(ClientResourceRequest query,
IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.LastConnection >= query.ClientConnected);
return clients;
}
private static IQueryable<ClientAlias> SearchByGame(ClientResourceRequest query, IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.GameName == query.GameName);
return clients;
}
}

View File

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

View File

@ -0,0 +1,470 @@
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;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
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 IRConParser parser;
private IRConParserConfiguration config;
private readonly ILogger _log;
private readonly Encoding _gameEncoding;
public RConConnection(string ipAddress, int port, string password, ILogger<RConConnection> log, Encoding gameEncoding)
{
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
_gameEncoding = gameEncoding;
RConPassword = password;
_log = log;
}
public void SetConfiguration(IRConParser parser)
{
this.parser = parser;
config = parser.Configuration;
}
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];
_log.LogDebug("Waiting for semaphore to be released [{endpoint}]", Endpoint);
// 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;
_log.LogDebug("Semaphore has been released [{endpoint}]", Endpoint);
_log.LogDebug("Query {@queryInfo}", new { endpoint=Endpoint.ToString(), type, parameters });
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 ex)
{
connectionState.OnComplete.Release(1);
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogError(ex, "Could not convert RCon data payload to desired encoding {encoding} {params}",
_gameEncoding.EncodingName, parameters);
}
throw new RConException($"Invalid character encountered when converting encodings");
}
byte[][] response = null;
retrySend:
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({connectionAttempts}/{allowedConnectionFailures} attempts) with parameters {payload}",
connectionState.ConnectionAttempts,
StaticHelpers.AllowedConnectionFails, parameters);
}
}
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = false,
Ttl = 100,
ExclusiveAddressUse = true,
})
{
connectionState.SendEventArgs.UserToken = socket;
connectionState.OnSentData.Reset();
connectionState.OnReceivedData.Reset();
connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear();
bool exceptionCaught = false;
_log.LogDebug("Sending {payloadLength} bytes to [{endpoint}] ({connectionAttempts}/{allowedConnectionFailures})",
payload.Length, Endpoint, connectionState.ConnectionAttempts, StaticHelpers.AllowedConnectionFails);
try
{
response = await SendPayloadAsync(payload, waitForResponse, parser.OverrideTimeoutForCommand(parameters));
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
{
throw new RConException("Expected response but got 0 bytes back");
}
connectionState.ConnectionAttempts = 0;
}
catch
{
// we want to retry with a delay
if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
{
exceptionCaught = true;
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
goto retrySend;
}
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Made {connectionAttempts} attempts to send RCon data to server, but received no response",
connectionState.ConnectionAttempts);
}
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
}
finally
{
// we don't want to release if we're going to retry the query
if (connectionState.OnComplete.CurrentCount == 0 && !exceptionCaught)
{
connectionState.OnComplete.Release(1);
}
}
}
if (response.Length == 0)
{
_log.LogDebug("Received empty response for RCon request {@query}", new { endpoint=Endpoint.ToString(), type, parameters });
return new string[0];
}
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) : RecombineMessages(response);
// note: not all games respond if the pasword is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{
throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
}
if (responseString.Contains("rcon_password"))
{
throw new RConException(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)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Invalid response header from server. Expected {expected}, but got {response}",
config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
}
throw new RConException("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);
}
/// <summary>
/// Recombines multiple game messages into one
/// </summary>
/// <param name="payload"></param>
/// <returns></returns>
private string RecombineMessages(byte[][] payload)
{
if (payload.Length == 1)
{
return _gameEncoding.GetString(payload[0]).TrimEnd('\n') + '\n';
}
else
{
var builder = new StringBuilder();
for (int i = 0; i < payload.Length; i++)
{
string message = _gameEncoding.GetString(payload[i]).TrimEnd('\n') + '\n';
if (i > 0)
{
message = message.Replace(config.CommandPrefixes.RConResponse, "");
}
builder.Append(message);
}
builder.Append('\n');
return builder.ToString();
}
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout)
{
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 asynchronously
// this really shouldn't ever happen because it's UDP
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout(1))))
{
using(LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Socket timed out while sending RCon data on attempt {attempt}",
connectionState.ConnectionAttempts);
}
rconSocket.Close();
throw new NetworkException("Timed out sending RCon 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)
{
_log.LogDebug("Waiting to asynchronously receive data on attempt #{connectionAttempts}", connectionState.ConnectionAttempts);
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(
new[]
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts),
overrideTimeout
}.Max())))
{
if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Socket timed out while waiting for RCon response on attempt {attempt} with timeout delay of {timeout}",
connectionState.ConnectionAttempts,
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
}
}
rconSocket.Close();
throw new NetworkException("Timed out receiving RCon response", rconSocket);
}
}
rconSocket.Close();
return GetResponseData(connectionState);
}
private byte[][] GetResponseData(ConnectionState connectionState)
{
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)
{
_log.LogDebug("Read {bytesTransferred} bytes from {endpoint}", e.BytesTransferred, e.RemoteEndPoint);
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
_log.LogDebug("No bytes were transmitted so the connection was probably closed");
ActiveQueries[this.Endpoint].OnReceivedData.Set();
return;
}
if (!(sender is Socket sock))
{
return;
}
var state = ActiveQueries[this.Endpoint];
state.BytesReadPerSegment.Add(e.BytesTransferred);
// I don't even want to know why this works for getting more data from Cod4x
// but I'm leaving it in here as long as it doesn't break anything.
// it's very stupid...
Thread.Sleep(150);
try
{
var totalBytesTransferred = e.BytesTransferred;
_log.LogDebug("{total} total bytes transferred with {available} bytes remaining", totalBytesTransferred, sock.Available);
// we still have available data so the payload was segmented
while (sock.Available > 0)
{
_log.LogDebug("{available} more bytes to be read", sock.Available);
var bufferSpaceAvailable = sock.Available + totalBytesTransferred - state.ReceiveBuffer.Length;
if (bufferSpaceAvailable >= 0 )
{
_log.LogWarning("Not enough buffer space to store incoming data {bytesNeeded} additional bytes required", bufferSpaceAvailable);
}
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
if (sock.ReceiveAsync(state.ReceiveEventArgs))
{
_log.LogDebug("Remaining bytes are async");
continue;
}
_log.LogDebug("Read {bytesTransferred} synchronous bytes from {endpoint}", state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint);
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
}
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
catch (ObjectDisposedException)
{
_log.LogDebug("Socket was disposed while receiving data");
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
_log.LogDebug("Sent {byteCount} bytes to {endpoint}", e.Buffer?.Length, e.ConnectSocket?.RemoteEndPoint);
ActiveQueries[this.Endpoint].OnSentData.Set();
}
}
}

View File

@ -1,84 +0,0 @@
using System;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System.Collections.Generic;
using System.Globalization;
using SharedLibraryCore.Formatting;
namespace IW4MAdmin.Application.RConParsers
{
/// <summary>
/// generic implementation of the IRConParserConfiguration
/// allows script plugins to generate dynamic RCon configurations
/// </summary>
public class DynamicRConParserConfiguration : IRConParserConfiguration
{
public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; }
public ParserRegex HostnameStatus { get; set; }
public ParserRegex MaxPlayersStatus { get; set; }
public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; }
public bool WaitForResponse { get; set; } = true;
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
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, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
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 bool ShouldRemoveDiacritics { get; set; }
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)
{
Status = parserRegexFactory.CreateParserRegex();
MapStatus = parserRegexFactory.CreateParserRegex();
GametypeStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex();
StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
const string mapRotateCommand = "map_rotate";
const string mapCommand = "map";
const string fastRestartCommand = "fast_restart";
const string quitCommand = "quit";
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
{
if (!OverrideCommandTimeouts.ContainsKey(command))
{
OverrideCommandTimeouts.Add(command, 45);
}
}
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
{
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
}
}
}
}

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

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