implement new eventing system

This commit is contained in:
RaidMax 2023-04-05 09:54:57 -05:00
parent 2e726ea9ed
commit f41ce39180
39 changed files with 1410 additions and 526 deletions

View File

@ -21,7 +21,6 @@
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

View File

@ -16,6 +16,7 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
@ -23,12 +24,18 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
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 ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@ -50,7 +57,7 @@ namespace IW4MAdmin.Application
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public CancellationToken CancellationToken => _isRunningTokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
@ -64,29 +71,30 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private CancellationTokenSource _tokenSource;
private CancellationTokenSource _isRunningTokenSource;
private CancellationTokenSource _eventHandlerTokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler;
private readonly ICoreEventHandler _coreEventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -101,14 +109,14 @@ namespace IW4MAdmin.Application
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_tokenSource = new CancellationTokenSource();
_isRunningTokenSource = new CancellationTokenSource();
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_eventHandler = eventHandler;
_coreEventHandler = coreEventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
@ -117,6 +125,8 @@ namespace IW4MAdmin.Application
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
}
public IEnumerable<IPlugin> Plugins { get; }
@ -124,7 +134,7 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent)
{
ProcessingEvents.TryAdd(newEvent.Id, newEvent);
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
// the event has failed already
if (newEvent.Failed)
@ -142,12 +152,12 @@ namespace IW4MAdmin.Application
catch (TaskCanceledException)
{
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
// this happens if a plugin requires login
@ -186,11 +196,11 @@ namespace IW4MAdmin.Application
}
skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
@ -199,14 +209,16 @@ namespace IW4MAdmin.Application
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.Id, out _);
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
if (ProcessingEvents.Values.Count(gameEvent =>
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
newEvent.CorrelationId is null)
{
ProcessingEvents.Remove(newEvent.Id, out _);
ProcessingEvents.Remove(newEvent.IncrementalId, out _);
}
// tell anyone waiting for the output that we're done
@ -226,75 +238,58 @@ namespace IW4MAdmin.Application
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
public async Task UpdateServerStates()
private Task UpdateServerStates()
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
var timeout = TimeSpan.FromSeconds(60);
while (!_tokenSource.IsCancellationRequested) // main shutdown requested
var index = 0;
return Task.WhenAll(_servers.Select(server =>
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.task.IsCompleted)
.Select(ut => ut.Key)
.ToList();
// remove the update tasks as they have completed
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
{
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
{
runningUpdateTasks[serverId].tokenSource.Cancel();
}
runningUpdateTasks.Remove(serverId);
}
// select the servers where the tasks have completed
var newTaskServers = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
{
var firstTokenSource = new CancellationTokenSource();
firstTokenSource.CancelAfter(timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(firstTokenSource.Token, _tokenSource.Token);
runningUpdateTasks.Add(server.EndPoint, (ProcessUpdateHandler(server, linkedTokenSource.Token), linkedTokenSource, DateTime.Now));
}
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
}
// if a cancellation is received, we want to return immediately after shutting down
catch
{
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
break;
}
}
var thisIndex = index;
Interlocked.Increment(ref index);
return ProcessUpdateHandler(server, thisIndex);
}));
}
private async Task ProcessUpdateHandler(Server server, CancellationToken token)
private async Task ProcessUpdateHandler(Server server, int index)
{
try
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)
{
await server.ProcessUpdatesAsync(token);
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
try
{
_logger.LogError(ex, "Failed to update status");
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
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)
{
// ignored
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.Id))
{
_logger.LogError(ex, "Failed to update status");
}
}
finally
{
server.IsInitialized = true;
}
}
finally
{
server.IsInitialized = true;
}
// run the final updates to clean up server
await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
}
public async Task Init()
@ -305,18 +300,24 @@ namespace IW4MAdmin.Application
#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);
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;
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
# endregion
#region PLUGINS
foreach (var plugin in Plugins)
{
try
{
if (plugin is ScriptPlugin scriptPlugin)
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
@ -391,13 +392,11 @@ namespace IW4MAdmin.Application
if (string.IsNullOrEmpty(_appConfig.Id))
{
_appConfig.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save();
}
#pragma warning disable 618
@ -442,8 +441,8 @@ namespace IW4MAdmin.Application
serverConfig.ModifyParsers();
}
await ConfigHandler.Save();
}
await ConfigHandler.Save();
}
if (_appConfig.Servers.Length == 0)
@ -468,7 +467,7 @@ namespace IW4MAdmin.Application
#endregion
#region COMMANDS
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
@ -543,26 +542,23 @@ namespace IW4MAdmin.Application
try
{
// todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
using (LogContext.PushProperty("Server", ServerInstance.ToString()))
var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
using (LogContext.PushProperty("Server", serverInstance!.ToString()))
{
_logger.LogInformation("Beginning server communication initialization");
await ServerInstance.Initialize();
await serverInstance.Initialize();
_servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname);
_servers.Add(serverInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname);
}
// add the start event for this server
var e = new GameEvent()
QueueEvent(new MonitorStartEvent
{
Type = EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
Server = serverInstance,
Source = this
});
AddEvent(e);
successServers++;
}
@ -593,11 +589,27 @@ namespace IW4MAdmin.Application
}
}
public async Task Start() => await UpdateServerStates();
public async Task Start()
{
_eventHandlerTokenSource = new CancellationTokenSource();
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)
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
{
try
{
@ -609,17 +621,30 @@ namespace IW4MAdmin.Application
}
}
_tokenSource.Cancel();
_isRunningTokenSource.Cancel();
IsRunning = false;
}
public void Restart()
public async Task Restart()
{
IsRestartRequested = true;
Stop().GetAwaiter().GetResult();
_tokenSource.Dispose();
_tokenSource = new CancellationTokenSource();
await 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]
@ -661,7 +686,12 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent)
{
_eventHandler.HandleEvent(this, gameEvent);
_coreEventHandler.QueueEvent(this, gameEvent);
}
public void QueueEvent(CoreEvent coreEvent)
{
_coreEventHandler.QueueEvent(this, coreEvent);
}
public IPageList GetPageList()
@ -698,15 +728,132 @@ namespace IW4MAdmin.Application
public void AddAdditionalCommand(IManagerCommand command)
{
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
lock (_commands)
{
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {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})");
}
_commands.Add(command);
_commands.Add(command);
}
}
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 async Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
{
if (requestEvent.Server is not IW4MServer server)
{
return;
}
var completed = false;
try
{
if (requestEvent.DelayMs.HasValue)
{
await Task.Delay(requestEvent.DelayMs.Value, token);
}
if (requestEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(requestEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await server.SetDvarAsync(requestEvent.ValueName, requestEvent.Value, token);
completed = true;
}
catch
{
// ignored
}
finally
{
QueueEvent(new ServerValueSetCompleteEvent
{
Server = server,
Source = server,
Success = completed,
Value = requestEvent.Value,
ValueName = requestEvent.ValueName
});
}
}
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

@ -0,0 +1,145 @@
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);
}
}
}

View File

@ -9,7 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
@ -31,6 +31,9 @@ using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Interfaces.Events;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@ -44,11 +47,12 @@ namespace IW4MAdmin
private const int REPORT_FLAG_COUNT = 4;
private long lastGameTime = 0;
public int Id { get; private set; }
private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
private readonly ILookupCache<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration;
private EFServer _cachedDatabaseServer;
private readonly StatManager _statManager;
public IW4MServer(
ServerConfiguration serverConfiguration,
@ -72,6 +76,18 @@ namespace IW4MAdmin
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
_statManager = serviceProvider.GetRequiredService<StatManager>();
IGameServerEventSubscriptions.MonitoringStarted += async (gameEvent, token) =>
{
if (gameEvent.Server.Id != Id)
{
return;
}
await EnsureServerAdded();
await _statManager.EnsureServerAdded(gameEvent.Server, token);
};
}
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -108,7 +124,7 @@ namespace IW4MAdmin
Clients[client.ClientNumber] = client;
ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
var e = new GameEvent()
var e = new GameEvent
{
Origin = client,
Owner = this,
@ -116,6 +132,11 @@ namespace IW4MAdmin
};
Manager.AddEvent(e);
Manager.QueueEvent(new ClientStateInitializeEvent
{
Client = client,
Source = this,
});
return client;
}
@ -210,10 +231,17 @@ namespace IW4MAdmin
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString());
await cmd.ExecuteAsync(E);
Manager.QueueEvent(new ClientExecuteCommandEvent
{
Command = cmd,
Client = E.Origin,
Source = this,
CommandText = E.Data
});
}
var pluginTasks = Manager.Plugins
.Select(async plugin => await CreatePluginTask(plugin, E));
var pluginTasks = Manager.Plugins.Where(plugin => !plugin.IsParser)
.Select(plugin => CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
}
@ -250,7 +278,7 @@ namespace IW4MAdmin
try
{
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
await plugin.OnEventAsync(gameEvent, this);
}
catch (OperationCanceledException)
{
@ -277,29 +305,7 @@ namespace IW4MAdmin
{
ServerLogger.LogDebug("processing event of type {type}", E.Type);
if (E.Type == GameEvent.EventType.Start)
{
var existingServer = (await _serverCache
.FirstAsync(server => server.Id == EndPoint));
var serverId = await GetIdForServer(E.Owner);
if (existingServer == null)
{
var server = new EFServer()
{
Port = Port,
EndPoint = ToString(),
ServerId = serverId,
GameName = (Reference.Game?)GameName,
HostName = Hostname
};
await _serverCache.AddAsync(server);
}
}
else if (E.Type == GameEvent.EventType.ConnectionLost)
if (E.Type == GameEvent.EventType.ConnectionLost)
{
var exception = E.Extra as Exception;
ServerLogger.LogError(exception,
@ -350,9 +356,18 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.ChangePermission)
{
var newPermission = (Permission) E.Extra;
var oldPermission = E.Target.Level;
ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
E.Origin.ToString(), E.Target.ToString(), newPermission);
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
Manager.QueueEvent(new ClientPermissionChangeEvent
{
Client = E.Origin,
Source = this,
OldPermission = oldPermission,
NewPermission = newPermission
});
}
else if (E.Type == GameEvent.EventType.Connect)
@ -500,6 +515,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin);
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = E.Target,
Penalty = newPenalty
});
}
else if (E.Type == GameEvent.EventType.Unflag)
@ -519,6 +540,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty);
Manager.QueueEvent(new ClientPenaltyRevokeEvent
{
Client = E.Target,
Penalty = unflagPenalty
});
}
else if (E.Type == GameEvent.EventType.Report)
@ -554,6 +581,13 @@ namespace IW4MAdmin
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
}
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = E.Target,
Penalty = newReport,
Source = this
});
}
else if (E.Type == GameEvent.EventType.TempBan)
@ -729,6 +763,11 @@ namespace IW4MAdmin
MaxClients = int.Parse(dict["com_maxclients"]);
}
else if (dict.ContainsKey("com_maxplayers"))
{
MaxClients = int.Parse(dict["com_maxplayers"]);
}
if (dict.ContainsKey("mapname"))
{
UpdateMap(dict["mapname"]);
@ -773,34 +812,6 @@ namespace IW4MAdmin
E.Origin.UpdateTeam(E.Extra as string);
}
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentClientGuid")
{
var parts = E.Data.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 Manager.GetPenaltyService()
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
{
ServerLogger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
E.Origin.ToString(), guid);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
Utilities.IW4MAdminClient(this), true);
}
}
}
}
lock (ChatHistory)
{
while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
@ -820,6 +831,53 @@ namespace IW4MAdmin
}
}
public async Task EnsureServerAdded()
{
var gameServer = await _serverCache
.FirstAsync(server => server.EndPoint == base.Id);
if (gameServer == null)
{
gameServer = new EFServer
{
Port = ListenPort,
EndPoint = base.Id,
ServerId = BuildLegacyDatabaseId(),
GameName = (Reference.Game?)GameName,
HostName = ServerName
};
await _serverCache.AddAsync(gameServer);
}
await using var context = _serviceProvider.GetRequiredService<IDatabaseContextFactory>()
.CreateContext(enableTracking: false);
context.Servers.Attach(gameServer);
// we want to set the gamename up if it's never been set, or it changed
if (!gameServer.GameName.HasValue || gameServer.GameName.Value != GameCode)
{
gameServer.GameName = GameCode;
context.Entry(gameServer).Property(property => property.GameName).IsModified = true;
}
if (gameServer.HostName == null || gameServer.HostName != ServerName)
{
gameServer.HostName = ServerName;
context.Entry(gameServer).Property(property => property.HostName).IsModified = true;
}
if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword))
{
gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword);
context.Entry(gameServer).Property(property => property.IsPasswordProtected).IsModified = true;
}
await context.SaveChangesAsync();
_cachedDatabaseServer = gameServer;
}
private async Task OnClientUpdate(EFClient origin)
{
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
@ -910,21 +968,14 @@ namespace IW4MAdmin
{
server ??= this;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
}
return (await _serverCache.FirstAsync(cachedServer =>
cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
}
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id;
var serverId = (await _serverCache
.FirstAsync(_server => _server.ServerId == server.EndPoint ||
_server.EndPoint == server.ToString() ||
_server.ServerId == id))?.ServerId;
return !serverId.HasValue ? id : serverId.Value;
private long BuildLegacyDatabaseId()
{
long id = HashCode.Combine(ListenAddress, ListenPort);
return id < 0 ? Math.Abs(id) : id;
}
private void UpdateMap(string mapname)
@ -983,7 +1034,7 @@ namespace IW4MAdmin
{
await client.OnDisconnect();
var e = new GameEvent()
var e = new GameEvent
{
Type = GameEvent.EventType.Disconnect,
Owner = this,
@ -994,6 +1045,14 @@ namespace IW4MAdmin
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
Manager.QueueEvent(new MonitorStopEvent
{
Server = this
});
}
private DateTime _lastMessageSent = DateTime.Now;
@ -1075,6 +1134,16 @@ namespace IW4MAdmin
Manager.AddEvent(gameEvent);
}
if (polledClients[2].Any())
{
Manager.QueueEvent(new ClientDataUpdateEvent
{
Clients = new ReadOnlyCollection<EFClient>(polledClients[2]),
Server = this,
Source = this,
});
}
if (Throttled)
{
var gameEvent = new GameEvent
@ -1086,6 +1155,12 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionRestoreEvent
{
Server = this,
Source = this
});
}
LastPoll = DateTime.Now;
@ -1109,6 +1184,12 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionInterruptEvent
{
Server = this,
Source = this
});
return true;
}
finally
@ -1469,6 +1550,12 @@ namespace IW4MAdmin
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
activeClient.CurrentServer.Broadcast(message);
}
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = targetClient,
Penalty = newPenalty
});
}
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
@ -1507,6 +1594,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = targetClient,
Penalty = newPenalty
});
}
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
@ -1540,6 +1633,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = targetClient,
Penalty = newPenalty
});
}
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
@ -1575,6 +1674,12 @@ namespace IW4MAdmin
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = targetClient,
Penalty = newPenalty
});
}
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
@ -1596,6 +1701,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
Manager.QueueEvent(new ClientPenaltyRevokeEvent
{
Client = targetClient,
Penalty = unbanPenalty
});
}
public override void InitializeTokens()
@ -1605,5 +1716,7 @@ namespace IW4MAdmin
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
}
public override long LegacyDatabaseId => _cachedDatabaseServer.ServerId;
}
}

View File

@ -51,7 +51,7 @@ namespace IW4MAdmin.Application
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager;
private static Task _applicationTask;
private static ServiceProvider _serviceProvider;
private static IServiceProvider _serviceProvider;
/// <summary>
/// entrypoint of the application
@ -112,23 +112,24 @@ namespace IW4MAdmin.Application
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services...");
var services = await ConfigureServices(args);
_serviceProvider = services.BuildServiceProvider();
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
_applicationTask = RunApplicationTasksAsync(logger, services);
var tasks = new[]
{
versionChecker.CheckVersion(),
_applicationTask
};
logger.LogDebug("Configuring services...");
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await configHandler.BuildAsync();
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
await _serverManager.Init();
await Task.WhenAll(tasks);
_applicationTask = Task.WhenAll(RunApplicationTasksAsync(logger, _serviceProvider),
_serverManager.Start());
await _applicationTask;
logger.LogInformation("Shutdown completed successfully");
}
catch (Exception e)
@ -178,21 +179,20 @@ namespace IW4MAdmin.Application
{
goto restart;
}
await _serviceProvider.DisposeAsync();
}
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
private static Task RunApplicationTasksAsync(ILogger logger, IServiceProvider serviceProvider)
{
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken)
: Task.CompletedTask;
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
// 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
@ -203,18 +203,15 @@ namespace IW4MAdmin.Application
var tasks = new[]
{
versionChecker.CheckVersion(),
webfrontTask,
_serverManager.Start(),
_serviceProvider.GetRequiredService<IMasterCommunication>()
serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
};
logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully");
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
return Task.WhenAll(tasks);
}
/// <summary>
@ -302,8 +299,21 @@ namespace IW4MAdmin.Application
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
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
@ -351,13 +361,11 @@ namespace IW4MAdmin.Application
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static async Task<IServiceCollection> ConfigureServices(string[] args)
private static void ConfigureServices(IServiceCollection serviceCollection)
{
// todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>()
@ -365,14 +373,10 @@ namespace IW4MAdmin.Application
// for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await appConfigHandler.BuildAsync();
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultConfigHandler.BuildAsync();
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
await commandConfigHandler.BuildAsync();
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
await statsCommandHandler.BuildAsync();
var defaultConfig = defaultConfigHandler.Configuration();
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
@ -385,13 +389,6 @@ namespace IW4MAdmin.Application
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
await appConfigHandler.Save();
}
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
@ -402,17 +399,10 @@ namespace IW4MAdmin.Application
}
// build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig)
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
@ -464,7 +454,9 @@ namespace IW4MAdmin.Application
.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())
@ -473,18 +465,9 @@ namespace IW4MAdmin.Application
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
serviceCollection.AddSource();
return serviceCollection;
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
}
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -27,7 +28,7 @@ public class RemoteCommandService : IRemoteCommandService
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
var (success, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
return result;
}
@ -56,7 +57,8 @@ public class RemoteCommandService : IRemoteCommandService
: $"{_appConfig.CommandPrefix}{command}",
Origin = client,
Owner = server,
IsRemote = true
IsRemote = true,
CorrelationId = Guid.NewGuid()
};
server.Manager.AddEvent(remoteEvent);
@ -72,7 +74,7 @@ public class RemoteCommandService : IRemoteCommandService
{
response = new[]
{
new CommandResponseInfo()
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
@ -90,7 +92,7 @@ public class RemoteCommandService : IRemoteCommandService
}
}
catch (System.OperationCanceledException)
catch (OperationCanceledException)
{
response = new[]
{

View File

@ -12,8 +12,10 @@ 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
{
@ -24,28 +26,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
private readonly IEventPublisher _eventPublisher;
private bool _inProgress;
private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
IManager manager, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
_eventPublisher = eventPublisher;
_eventPublisher.OnClientConnect += SaveConnectionInfo;
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
}
~ServerDataCollector()
{
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
@ -131,18 +125,19 @@ namespace IW4MAdmin.Application.Misc
await context.SaveChangesAsync(token);
}
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
private async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
{
using var context = _contextFactory.CreateContext(enableTracking: false);
await using var context = _contextFactory.CreateContext(enableTracking: false);
context.ConnectionHistory.Add(new EFClientConnectionHistory
{
ClientId = gameEvent.Origin.ClientId,
ServerId = gameEvent.Owner.GetIdForServer().Result,
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
ClientId = stateEvent.Client.ClientId,
ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
ConnectionType = stateEvent is ClientStateAuthorizeEvent
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
context.SaveChanges();
await context.SaveChangesAsync();
}
}
}

View File

@ -176,7 +176,7 @@ namespace IW4MAdmin.Application.Misc
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{

View File

@ -151,10 +151,12 @@ public class ScriptPluginV2 : IPluginV2
}
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())
{

View File

@ -142,12 +142,12 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
});
}
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string? clientName,
string? ipAddress)
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
string ipAddress)
{
return group =>
{
ClientResourceResponse? match = null;
ClientResourceResponse match = null;
var lowercaseClientName = clientName?.ToLower();
if (!string.IsNullOrWhiteSpace(lowercaseClientName))

View File

@ -43,7 +43,7 @@ namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers
[HttpGet]
[Route("Radar/{serverId}/Map")]
public async Task<IActionResult> Map(string serverId = null)
public IActionResult Map(string serverId = null)
{
var server = serverId == null
? _manager.GetServers().FirstOrDefault()

View File

@ -59,7 +59,9 @@ public class Plugin : IPluginV2
return true;
}
var muteMeta = _muteManager.GetCurrentMuteState(gameEvent.Origin).GetAwaiter().GetResult();
var muteMeta = Task.Run(() => _muteManager.GetCurrentMuteState(gameEvent.Origin), cancellationToken)
.GetAwaiter().GetResult();
if (muteMeta.MuteState is not MuteState.Muted)
{
return true;

View File

@ -196,7 +196,7 @@ const plugin = {
let data = [];
const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
const metaService = this.serviceResolver.resolveService('IMetaServiceV2');
if (event.subType === 'Meta') {
const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result;
@ -237,8 +237,8 @@ const plugin = {
this.logger.logDebug('ClientId={clientId}', clientId);
if (clientId == null) {
this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType);
if (clientId == null || isNaN(clientId)) {
this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}: {EventData}', event.clientNumber, event.eventType, event.data);
this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
ClientNumber: event.clientNumber
}, undefined, {

View File

@ -0,0 +1,12 @@
using System;
namespace SharedLibraryCore.Events;
public abstract class CoreEvent
{
public Guid Id { get; } = Guid.NewGuid();
public Guid? CorrelationId { get; init; }
public object Source { get; init; }
public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow;
public DateTimeOffset? ProcessedAt { get; set; }
}

View File

@ -1,88 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using SharedLibraryCore.Dtos;
namespace SharedLibraryCore.Events
{
public class EventApi
{
private const int MaxEvents = 25;
private static readonly ConcurrentQueue<EventInfo> RecentEvents = new ConcurrentQueue<EventInfo>();
public static IEnumerable<EventInfo> GetEvents(bool shouldConsume)
{
var eventList = RecentEvents.ToArray();
// clear queue if events should be consumed
if (shouldConsume)
{
RecentEvents.Clear();
}
return eventList;
}
public static void OnGameEvent(GameEvent gameEvent)
{
var E = gameEvent;
// don't want to clog up the api with unknown events
if (E.Type == GameEvent.EventType.Unknown)
{
return;
}
var apiEvent = new EventInfo
{
ExtraInfo = E.Extra?.ToString() ?? E.Data,
GameInfo = new EntityInfo
{
Name = E.Owner.GameName.ToString(),
Id = (int)E.Owner.GameName
},
OwnerEntity = new EntityInfo
{
Name = E.Owner.Hostname,
Id = E.Owner.EndPoint
},
OriginEntity = E.Origin == null
? null
: new EntityInfo
{
Id = E.Origin.ClientId,
Name = E.Origin.Name
},
TargetEntity = E.Target == null
? null
: new EntityInfo
{
Id = E.Target.ClientId,
Name = E.Target.Name
},
EventType = new EntityInfo
{
Id = (int)E.Type,
Name = E.Type.ToString()
},
EventTime = E.Time
};
// add the new event to the list
AddNewEvent(apiEvent);
}
/// <summary>
/// Adds event to the list and removes first added if reached max capacity
/// </summary>
/// <param name="info">EventInfo to add</param>
private static void AddNewEvent(EventInfo info)
{
// remove the first added event
if (RecentEvents.Count >= MaxEvents)
{
RecentEvents.TryDequeue(out _);
}
RecentEvents.Enqueue(info);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace SharedLibraryCore.Events;
public static class EventExtensions
{
public static Task InvokeAsync<TEventType>(this Func<TEventType, CancellationToken, Task> function,
TEventType eventArgType, CancellationToken token)
{
if (function is null)
{
return Task.CompletedTask;
}
return Task.WhenAll(function.GetInvocationList().Cast<Func<TEventType, CancellationToken, Task>>()
.Select(x => RunHandler(x, eventArgType, token)));
}
private static async Task RunHandler<TEventType>(Func<TEventType, CancellationToken, Task> handler,
TEventType eventArgType, CancellationToken token)
{
if (token == CancellationToken.None)
{
// special case to allow tasks like request after delay to run longer
await handler(eventArgType, token);
}
using var timeoutToken = new CancellationTokenSource(Utilities.DefaultCommandTimeout);
using var tokenSource =
CancellationTokenSource.CreateLinkedTokenSource(token, timeoutToken.Token);
try
{
await handler(eventArgType, tokenSource.Token);
}
catch (Exception)
{
// ignored
}
}
}

View File

@ -4,5 +4,5 @@ namespace SharedLibraryCore.Events.Game;
public abstract class GameEventV2 : GameEvent
{
public IGameServer Server { get; init; }
public IGameServer Server => Owner;
}

View File

@ -5,10 +5,11 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
namespace SharedLibraryCore
{
public class GameEvent
public class GameEvent : CoreEvent
{
public enum EventFailReason
{
@ -134,6 +135,8 @@ namespace SharedLibraryCore
/// </summary>
ConnectionRestored,
SayTeam = 99,
// events "generated" by clients
/// <summary>
/// a client sent a message
@ -267,7 +270,7 @@ namespace SharedLibraryCore
public GameEvent()
{
Time = DateTime.UtcNow;
Id = GetNextEventId();
IncrementalId = GetNextEventId();
}
~GameEvent()
@ -275,8 +278,6 @@ namespace SharedLibraryCore
_eventFinishedWaiter.Dispose();
}
public EventSource Source { get; set; }
/// <summary>
/// suptype of the event for more detailed classification
/// </summary>
@ -293,11 +294,10 @@ namespace SharedLibraryCore
public bool IsRemote { get; set; }
public object Extra { get; set; }
public DateTime Time { get; set; }
public long Id { get; }
public long IncrementalId { get; }
public EventFailReason FailReason { get; set; }
public bool Failed => FailReason != EventFailReason.None;
public Guid CorrelationId { get; set; } = Guid.NewGuid();
public List<string> Output { get; set; } = new List<string>();
public List<string> Output { get; set; } = new();
/// <summary>
/// Indicates if the event should block until it is complete
@ -328,23 +328,31 @@ namespace SharedLibraryCore
/// <returns>waitable task </returns>
public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token)
{
var processed = false;
Utilities.DefaultLogger.LogDebug("Begin wait for event {Id}", Id);
try
if (FailReason == EventFailReason.Timeout)
{
processed = await _eventFinishedWaiter.WaitAsync(timeSpan, token);
return this;
}
catch (TaskCanceledException)
var processed = false;
Utilities.DefaultLogger.LogDebug("Begin wait for {Name}, {Type}, {Id}", Type, GetType().Name,
IncrementalId);
try
{
await _eventFinishedWaiter.WaitAsync(timeSpan, token);
processed = true;
}
catch (OperationCanceledException)
{
processed = false;
}
finally
{
if (_eventFinishedWaiter.CurrentCount == 0)
if (processed)
{
_eventFinishedWaiter.Release();
Complete();
}
}

View File

@ -0,0 +1,14 @@
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Events.Management;
public class ClientPersistentIdReceiveEvent : ClientStateEvent
{
public ClientPersistentIdReceiveEvent(EFClient client, string persistentId)
{
Client = client;
PersistentId = persistentId;
}
public string PersistentId { get; init; }
}

View File

@ -0,0 +1,120 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Game;
namespace SharedLibraryCore.Interfaces.Events;
public interface IGameEventSubscriptions
{
/// <summary>
/// Raised when game log prints that match has started
/// <example>InitGame</example>
/// <value><see cref="MatchStartEvent"/></value>
/// </summary>
static event Func<MatchStartEvent, CancellationToken, Task> MatchStarted;
/// <summary>
/// Raised when game log prints that match has ended
/// <example>ShutdownGame:</example>
/// <value><see cref="MatchEndEvent"/></value>
/// </summary>
static event Func<MatchEndEvent, CancellationToken, Task> MatchEnded;
/// <summary>
/// Raised when game log printed that client has entered the match
/// <remarks>J;clientNetworkId;clientSlotNumber;clientName</remarks>
/// <example>J;110000100000000;0;bot</example>
/// <value><see cref="ClientEnterMatchEvent"/></value>
/// </summary>
public static event Func<ClientEnterMatchEvent, CancellationToken, Task> ClientEnteredMatch;
/// <summary>
/// Raised when game log prints that client has exited the match
/// <remarks>Q;clientNetworkId;clientSlotNumber;clientName</remarks>
/// <example>Q;110000100000000;0;bot</example>
/// <value><see cref="ClientExitMatchEvent"/></value>
/// </summary>
static event Func<ClientExitMatchEvent, CancellationToken, Task> ClientExitedMatch;
/// <summary>
/// Raised when game log prints that client has joined a team
/// <remarks>JT;clientNetworkId;clientSlotNumber;clientTeam;clientName</remarks>
/// <example>JT;110000100000000;0;axis;bot</example>
/// <value><see cref="ClientJoinTeamEvent"/></value>
/// </summary>
static event Func<ClientJoinTeamEvent, CancellationToken, Task> ClientJoinedTeam;
/// <summary>
/// Raised when game log prints that client has been damaged
/// <remarks>D;victimNetworkId;victimSlotNumber;victimTeam;victimName;attackerNetworkId;attackerSlotNumber;attackerTeam;attackerName;weapon;damage;meansOfDeath;hitLocation</remarks>
/// <example>D;110000100000000;17;axis;bot_0;110000100000001;4;allies;bot_1;scar_mp;38;MOD_HEAD_SHOT;head</example>
/// <value><see cref="ClientDamageEvent"/></value>
/// </summary>
static event Func<ClientDamageEvent, CancellationToken, Task> ClientDamaged;
/// <summary>
/// Raised when game log prints that client has been killed
/// <remarks>K;victimNetworkId;victimSlotNumber;victimTeam;victimName;attackerNetworkId;attackerSlotNumber;attackerTeam;attackerName;weapon;damage;meansOfDeath;hitLocation</remarks>
/// <example>K;110000100000000;17;axis;bot_0;110000100000001;4;allies;bot_1;scar_mp;100;MOD_HEAD_SHOT;head</example>
/// <value><see cref="ClientKillEvent"/></value>
/// </summary>
static event Func<ClientKillEvent, CancellationToken, Task> ClientKilled;
/// <summary>
/// Raised when game log prints that client entered a chat message
/// <remarks>say;clientNetworkId;clientSlotNumber;clientName;message</remarks>
/// <example>say;110000100000000;0;bot;hello world!</example>
/// <value><see cref="ClientMessageEvent"/></value>
/// </summary>
static event Func<ClientMessageEvent, CancellationToken, Task> ClientMessaged;
/// <summary>
/// Raised when game log prints that client entered a command (chat message prefixed with command character(s))
/// <remarks>say;clientNetworkId;clientSlotNumber;clientName;command</remarks>
/// <example>say;110000100000000;0;bot;!command</example>
/// <value><see cref="ClientCommandEvent"/></value>
/// </summary>
static event Func<ClientCommandEvent, CancellationToken, Task> ClientEnteredCommand;
/// <summary>
/// Raised when game log prints user generated script event
/// <remarks>GSE;data</remarks>
/// <example>GSE;loadBank=1</example>
/// <value><see cref="GameScriptEvent"/></value>
/// </summary>
static event Func<GameScriptEvent, CancellationToken, Task> ScriptEventTriggered;
static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
{
return coreEvent switch
{
MatchStartEvent matchStartEvent => MatchStarted?.InvokeAsync(matchStartEvent, token) ?? Task.CompletedTask,
MatchEndEvent matchEndEvent => MatchEnded?.InvokeAsync(matchEndEvent, token) ?? Task.CompletedTask,
ClientEnterMatchEvent clientEnterMatchEvent => ClientEnteredMatch?.InvokeAsync(clientEnterMatchEvent, token) ?? Task.CompletedTask,
ClientExitMatchEvent clientExitMatchEvent => ClientExitedMatch?.InvokeAsync(clientExitMatchEvent, token) ?? Task.CompletedTask,
ClientJoinTeamEvent clientJoinTeamEvent => ClientJoinedTeam?.InvokeAsync(clientJoinTeamEvent, token) ?? Task.CompletedTask,
ClientKillEvent clientKillEvent => ClientKilled?.InvokeAsync(clientKillEvent, token) ?? Task.CompletedTask,
ClientDamageEvent clientDamageEvent => ClientDamaged?.InvokeAsync(clientDamageEvent, token) ?? Task.CompletedTask,
ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask,
ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask,
GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask
};
}
static void ClearEventInvocations()
{
MatchStarted = null;
MatchEnded = null;
ClientEnteredMatch = null;
ClientExitedMatch = null;
ClientJoinedTeam = null;
ClientDamaged = null;
ClientKilled = null;
ClientMessaged = null;
ClientEnteredCommand = null;
ScriptEventTriggered = null;
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Server;
namespace SharedLibraryCore.Interfaces.Events;
public interface IGameServerEventSubscriptions
{
/// <summary>
/// Raised when IW4MAdmin starts monitoring a game server
/// <value><see cref="MonitorStartEvent"/></value>
/// </summary>
static event Func<MonitorStartEvent, CancellationToken, Task> MonitoringStarted;
/// <summary>
/// Raised when IW4MAdmin stops monitoring a game server
/// <value><see cref="MonitorStopEvent"/></value>
/// </summary>
static event Func<MonitorStopEvent, CancellationToken, Task> MonitoringStopped;
/// <summary>
/// Raised when communication was interrupted with a game server
/// <value><see cref="ConnectionInterruptEvent"/></value>
/// </summary>
static event Func<ConnectionInterruptEvent, CancellationToken, Task> ConnectionInterrupted;
/// <summary>
/// Raised when communication was resumed with a game server
/// <value><see cref="ConnectionRestoreEvent"/></value>
/// </summary>
static event Func<ConnectionRestoreEvent, CancellationToken, Task> ConnectionRestored;
/// <summary>
/// Raised when updated client data was received from a game server
/// <value><see cref="ClientDataUpdateEvent"/></value>
/// </summary>
static event Func<ClientDataUpdateEvent, CancellationToken, Task> ClientDataUpdated;
/// <summary>
/// Raised when a command was executed on a game server
/// <value><see cref="ServerCommandExecuteEvent"/></value>
/// </summary>
static event Func<ServerCommandExecuteEvent, CancellationToken, Task> ServerCommandExecuted;
/// <summary>
/// Raised when a server value is requested for a game server
/// <value><see cref="ServerValueRequestEvent"/></value>
/// </summary>
static event Func<ServerValueRequestEvent, CancellationToken, Task> ServerValueRequested;
/// <summary>
/// Raised when a server value was received from a game server (success or fail)
/// <value><see cref="ServerValueReceiveEvent"/></value>
/// </summary>
static event Func<ServerValueReceiveEvent, CancellationToken, Task> ServerValueReceived;
/// <summary>
/// Raised when a request to set a server value on a game server is received
/// <value><see cref="ServerValueSetRequestEvent"/></value>
/// </summary>
static event Func<ServerValueSetRequestEvent, CancellationToken, Task> ServerValueSetRequested;
/// <summary>
/// Raised when a setting server value on a game server is completed (success or fail)
/// <value><see cref="ServerValueSetRequestEvent"/></value>
/// </summary>
static event Func<ServerValueSetCompleteEvent, CancellationToken, Task> ServerValueSetCompleted;
static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
{
return coreEvent switch
{
MonitorStartEvent monitoringStartEvent => MonitoringStarted?.InvokeAsync(monitoringStartEvent, token) ?? Task.CompletedTask,
MonitorStopEvent monitorStopEvent => MonitoringStopped?.InvokeAsync(monitorStopEvent, token) ?? Task.CompletedTask,
ConnectionInterruptEvent connectionInterruptEvent => ConnectionInterrupted?.InvokeAsync(connectionInterruptEvent, token) ?? Task.CompletedTask,
ConnectionRestoreEvent connectionRestoreEvent => ConnectionRestored?.InvokeAsync(connectionRestoreEvent, token) ?? Task.CompletedTask,
ClientDataUpdateEvent clientDataUpdateEvent => ClientDataUpdated?.InvokeAsync(clientDataUpdateEvent, token) ?? Task.CompletedTask,
ServerCommandExecuteEvent dataReceiveEvent => ServerCommandExecuted?.InvokeAsync(dataReceiveEvent, token) ?? Task.CompletedTask,
ServerValueRequestEvent serverValueRequestEvent => ServerValueRequested?.InvokeAsync(serverValueRequestEvent, token) ?? Task.CompletedTask,
ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask,
ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask,
ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask
};
}
static void ClearEventInvocations()
{
MonitoringStarted = null;
MonitoringStopped = null;
ConnectionInterrupted = null;
ConnectionRestored = null;
ClientDataUpdated = null;
ServerCommandExecuted = null;
ServerValueReceived = null;
ServerValueRequested = null;
ServerValueSetRequested = null;
ServerValueSetCompleted = null;
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Management;
namespace SharedLibraryCore.Interfaces.Events;
public interface IManagementEventSubscriptions
{
/// <summary>
/// Raised when <see cref="IManager"/> is loading
/// </summary>
static event Func<IManager, CancellationToken, Task> Load;
/// <summary>
/// Raised when <see cref="IManager"/> is restarting
/// </summary>
static event Func<IManager, CancellationToken, Task> Unload;
/// <summary>
/// Raised when client enters a tracked state
/// <remarks>
/// At this point, the client is not guaranteed to be allowed to play on the server.
/// See <see cref="ClientStateAuthorized"/> for final state.
/// </remarks>
/// <value><see cref="ClientStateInitializeEvent"/></value>
/// </summary>
static event Func<ClientStateInitializeEvent, CancellationToken, Task> ClientStateInitialized;
/// <summary>
/// Raised when client enters an authorized state (valid data and no bans)
/// <value><see cref="ClientStateAuthorizeEvent"/></value>
/// </summary>
static event Func<ClientStateAuthorizeEvent, CancellationToken, Task> ClientStateAuthorized;
/// <summary>
/// Raised when client is no longer tracked (unknown state)
/// <remarks>At this point any references to the client should be dropped</remarks>
/// <value><see cref="ClientStateDisposeEvent"/></value>
/// </summary>
static event Func<ClientStateDisposeEvent, CancellationToken, Task> ClientStateDisposed;
/// <summary>
/// Raised when a client receives a penalty
/// <value><see cref="ClientPenaltyEvent"/></value>
/// </summary>
static event Func<ClientPenaltyEvent, CancellationToken, Task> ClientPenaltyAdministered;
/// <summary>
/// Raised when a client penalty is revoked (eg unflag/unban)
/// <value><see cref="ClientPenaltyRevokeEvent"/></value>
/// </summary>
static event Func<ClientPenaltyRevokeEvent, CancellationToken, Task> ClientPenaltyRevoked;
/// <summary>
/// Raised when a client command is executed (after completion of the command)
/// <value><see cref="ClientExecuteCommandEvent"/></value>
/// </summary>
static event Func<ClientExecuteCommandEvent, CancellationToken, Task> ClientCommandExecuted;
/// <summary>
/// Raised when a client's permission level changes
/// <value><see cref="ClientPermissionChangeEvent"/></value>
/// </summary>
static event Func<ClientPermissionChangeEvent, CancellationToken, Task> ClientPermissionChanged;
/// <summary>
/// Raised when a client logs in to the webfront or ingame
/// <value><see cref="LoginEvent"/></value>
/// </summary>
static event Func<LoginEvent, CancellationToken, Task> ClientLoggedIn;
/// <summary>
/// Raised when a client logs out of the webfront
/// <value><see cref="LogoutEvent"/></value>
/// </summary>
static event Func<LogoutEvent, CancellationToken, Task> ClientLoggedOut;
/// <summary>
/// Raised when a client's persistent id (stats file marker) is received
/// <value><see cref="ClientPersistentIdReceiveEvent"/></value>
/// </summary>
static event Func<ClientPersistentIdReceiveEvent, CancellationToken, Task> ClientPersistentIdReceived;
static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
{
return coreEvent switch
{
ClientStateInitializeEvent clientStateInitializeEvent => ClientStateInitialized?.InvokeAsync(
clientStateInitializeEvent, token) ?? Task.CompletedTask,
ClientStateDisposeEvent clientStateDisposedEvent => ClientStateDisposed?.InvokeAsync(
clientStateDisposedEvent, token) ?? Task.CompletedTask,
ClientStateAuthorizeEvent clientStateAuthorizeEvent => ClientStateAuthorized?.InvokeAsync(
clientStateAuthorizeEvent, token) ?? Task.CompletedTask,
ClientPenaltyRevokeEvent clientPenaltyRevokeEvent => ClientPenaltyRevoked?.InvokeAsync(
clientPenaltyRevokeEvent, token) ?? Task.CompletedTask,
ClientPenaltyEvent clientPenaltyEvent =>
ClientPenaltyAdministered?.InvokeAsync(clientPenaltyEvent, token) ?? Task.CompletedTask,
ClientPermissionChangeEvent clientPermissionChangeEvent => ClientPermissionChanged?.InvokeAsync(
clientPermissionChangeEvent, token) ?? Task.CompletedTask,
ClientExecuteCommandEvent clientExecuteCommandEvent => ClientCommandExecuted?.InvokeAsync(
clientExecuteCommandEvent, token) ?? Task.CompletedTask,
LogoutEvent logoutEvent => ClientLoggedOut?.InvokeAsync(logoutEvent, token) ?? Task.CompletedTask,
LoginEvent loginEvent => ClientLoggedIn?.InvokeAsync(loginEvent, token) ?? Task.CompletedTask,
ClientPersistentIdReceiveEvent clientPersistentIdReceiveEvent => ClientPersistentIdReceived?.InvokeAsync(
clientPersistentIdReceiveEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask
};
}
static Task InvokeLoadAsync(IManager manager, CancellationToken token) => Load?.InvokeAsync(manager, token) ?? Task.CompletedTask;
static Task InvokeUnloadAsync(IManager manager, CancellationToken token) => Unload?.InvokeAsync(manager, token) ?? Task.CompletedTask;
static void ClearEventInvocations()
{
Load = null;
Unload = null;
ClientStateInitialized = null;
ClientStateAuthorized = null;
ClientStateDisposed = null;
ClientPenaltyAdministered = null;
ClientPenaltyRevoked = null;
ClientCommandExecuted = null;
ClientPermissionChanged = null;
ClientLoggedIn = null;
ClientLoggedOut = null;
ClientPersistentIdReceived = null;
}
}

View File

@ -0,0 +1,19 @@
using System.Threading;
using SharedLibraryCore.Events;
namespace SharedLibraryCore.Interfaces;
/// <summary>
/// Handles games events (from log, manual events, etc)
/// </summary>
public interface ICoreEventHandler
{
/// <summary>
/// Add a core event event to the queue to be processed
/// </summary>
/// <param name="manager"><see cref="IManager"/></param>
/// <param name="coreEvent"><see cref="CoreEvent"/></param>
void QueueEvent(IManager manager, CoreEvent coreEvent);
void StartProcessing(CancellationToken token);
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Database.Models;
@ -16,7 +17,70 @@ namespace SharedLibraryCore.Interfaces
/// <param name="previousPenalty">previous penalty the kick is occuring for (if applicable)</param>
/// <returns></returns>
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
/// <summary>
/// Time the most recent match ended
/// </summary>
DateTime? MatchEndTime { get; }
/// <summary>
/// Time the current match started
/// </summary>
DateTime? MatchStartTime { get; }
/// <summary>
/// List of connected clients
/// </summary>
IReadOnlyList<EFClient> ConnectedClients { get; }
/// <summary>
/// Game code corresponding to the development studio project
/// </summary>
Reference.Game GameCode { get; }
/// <summary>
/// Indicates if the anticheat/custom callbacks/live radar integration is enabled
/// </summary>
bool IsLegacyGameIntegrationEnabled { get; }
/// <summary>
/// Unique identifier for the server (typically ip:port)
/// </summary>
string Id { get; }
/// <summary>
/// Network address the server is listening on
/// </summary>
string ListenAddress { get; }
/// <summary>
/// Network port the server is listening on
/// </summary>
int ListenPort { get; }
/// <summary>
/// Name of the server (hostname)
/// </summary>
string ServerName { get; }
/// <summary>
/// Current gametype
/// </summary>
string Gametype { get; }
/// <summary>
/// Game password (required to join)
/// </summary>
string GamePassword { get; }
/// <summary>
/// Current map the game server is running
/// </summary>
Map Map { get; }
/// <summary>
/// Database id for EFServer table and references
/// </summary>
long LegacyDatabaseId { get; }
}
}

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
@ -34,7 +35,7 @@ namespace SharedLibraryCore.Interfaces
Task Init();
Task Start();
Task Stop();
void Restart();
Task Restart();
[Obsolete]
ILogger GetLogger(long serverId);
@ -87,6 +88,11 @@ namespace SharedLibraryCore.Interfaces
/// <param name="gameEvent">event to be processed</param>
void AddEvent(GameEvent gameEvent);
/// <summary>
/// queues an event for processing
/// </summary>
void QueueEvent(CoreEvent coreEvent);
/// <summary>
/// adds an additional (script) command to the command list
/// </summary>

View File

@ -0,0 +1,11 @@
namespace SharedLibraryCore.Interfaces;
public interface IModularAssembly
{
string Name { get; }
string Author { get; }
string Version { get; }
string Scope => string.Empty;
string Role => string.Empty;
string[] Claims => System.Array.Empty<string>();
}

View File

@ -56,8 +56,6 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default);
/// <summary>
/// set value of DVAR by name
/// </summary>
@ -68,8 +66,6 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, CancellationToken token = default);
/// <summary>
/// executes a console command on the server
/// </summary>

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Localization;
namespace SharedLibraryCore.Database.Models
@ -604,6 +605,12 @@ namespace SharedLibraryCore.Database.Models
Utilities.DefaultLogger.LogInformation("Client {client} is leaving the game", ToString());
CurrentServer?.Manager.QueueEvent(new ClientStateDisposeEvent
{
Source = CurrentServer,
Client = this
});
try
{
await CurrentServer.Manager.GetClientService().Update(this);
@ -658,6 +665,11 @@ namespace SharedLibraryCore.Database.Models
};
CurrentServer.Manager.AddEvent(e);
CurrentServer.Manager.QueueEvent(new ClientStateAuthorizeEvent
{
Source = CurrentServer,
Client = this
});
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Threading;
@ -63,7 +64,7 @@ namespace SharedLibraryCore
{
Password = config.Password;
IP = config.IPAddress;
Port = config.Port;
ListenPort = config.Port;
Manager = mgr;
#pragma warning disable CS0612
Logger = deprecatedLogger ?? throw new ArgumentNullException(nameof(deprecatedLogger));
@ -89,8 +90,6 @@ namespace SharedLibraryCore
? Convert.ToInt64($"{ListenAddress!.Replace(".", "")}{ListenPort}")
: $"{ListenAddress!.Replace(".", "")}{ListenPort}".GetStableHashCode();
public long LegacyEndpoint => EndPoint;
public abstract long LegacyDatabaseId { get; }
public string Id => $"{ListenAddress}:{ListenPort}";
@ -105,6 +104,7 @@ namespace SharedLibraryCore
public List<ChatInfo> ChatHistory { get; protected set; }
public ClientHistoryInfo ClientHistory { get; }
public Game GameName { get; set; }
public Reference.Game GameCode => (Reference.Game)GameName;
public DateTime? MatchEndTime { get; protected set; }
public DateTime? MatchStartTime { get; protected set; }
@ -114,14 +114,17 @@ namespace SharedLibraryCore
protected set => hostname = value;
}
public string ServerName => Hostname;
public string Website { get; protected set; }
public string Gametype { get; set; }
public string GametypeName => DefaultSettings.Gametypes.FirstOrDefault(gt => gt.Game == GameName)?.Gametypes
public string GametypeName => DefaultSettings.Gametypes?.FirstOrDefault(gt => gt.Game == GameName)?.Gametypes
?.FirstOrDefault(gt => gt.Name == Gametype)?.Alias ?? Gametype;
public string GamePassword { get; protected set; }
public Map CurrentMap { get; set; }
public Map Map => CurrentMap;
public int ClientNum
{
@ -130,9 +133,13 @@ namespace SharedLibraryCore
public int MaxClients { get; protected set; }
public List<EFClient> Clients { get; protected set; }
public IReadOnlyList<EFClient> ConnectedClients =>
new ReadOnlyCollection<EFClient>(GetClientsAsList());
public string Password { get; }
public bool Throttled { get; protected set; }
public bool CustomCallback { get; protected set; }
public bool IsLegacyGameIntegrationEnabled => CustomCallback;
public string WorkingDirectory { get; protected set; }
public IRConConnection RemoteConnection { get; protected set; }
public IRConParser RconParser { get; set; }
@ -143,7 +150,7 @@ namespace SharedLibraryCore
// Internal
/// <summary>
/// this is actually the hostname now
/// this is actually the listen address now
/// </summary>
public string IP { get; protected set; }
@ -153,7 +160,7 @@ namespace SharedLibraryCore
public string Version { get; protected set; }
public bool IsInitialized { get; set; }
public int Port { get; }
public int ListenPort { get; }
public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty);
/// <summary>
@ -416,35 +423,6 @@ namespace SharedLibraryCore
public abstract Task<long> GetIdForServer(Server server = null);
public string GetServerDvar(string dvarName, int timeoutMs = 1000)
{
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{
return this.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
}
catch
{
return null;
}
}
public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000)
{
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{
this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
return true;
}
catch
{
return false;
}
}
public EFClient GetClientByNumber(int clientNumber) =>
GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber);
}

View File

@ -4,22 +4,22 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.10.13.1</Version>
<Version>2023.4.5.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<LangVersion>default</LangVersion>
<LangVersion>Preview</LangVersion>
<PackageTags>IW4MAdmin</PackageTags>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin/</RepositoryUrl>
<PackageProjectUrl>https://www.raidmax.org/IW4MAdmin/</PackageProjectUrl>
<Copyright>2022</Copyright>
<Copyright>2023</Copyright>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.10.13.1</PackageVersion>
<PackageVersion>2023.4.5.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

View File

@ -14,10 +14,13 @@ using System.Threading.Tasks;
using Data.Models;
using Humanizer;
using Humanizer.Localisation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization;
@ -26,7 +29,6 @@ using static SharedLibraryCore.Server;
using static Data.Models.Client.EFClient;
using static Data.Models.EFPenalty;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using RegionInfo = System.Globalization.RegionInfo;
namespace SharedLibraryCore
{
@ -43,7 +45,7 @@ namespace SharedLibraryCore
public static Encoding EncodingType;
public static Layout CurrentLocalization = new Layout(new Dictionary<string, string>());
public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, Utilities.IsDevelopment ? 360 : 25);
public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, /*Utilities.IsDevelopment ? 360 : */25);
public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!';
@ -66,6 +68,22 @@ namespace SharedLibraryCore
};
}
public static EFClient AsConsoleClient(this IGameServer server)
{
return new EFClient
{
ClientId = 1,
State = EFClient.ClientState.Connected,
Level = Permission.Console,
CurrentServer = server as Server,
CurrentAlias = new EFAlias
{
Name = "IW4MAdmin"
},
AdministeredPenalties = new List<EFPenalty>()
};
}
/// <summary>
/// fallback id for world events
/// </summary>
@ -95,14 +113,19 @@ namespace SharedLibraryCore
/// <summary>
/// caps client name to the specified character length - 3
/// and adds ellipses to the end of the reamining client name
/// and adds ellipses to the end of the remaining client name
/// </summary>
/// <param name="str">client name</param>
/// <param name="name">client name</param>
/// <param name="maxLength">max number of characters for the name</param>
/// <returns></returns>
public static string CapClientName(this string str, int maxLength)
public static string CapClientName(this string name, int maxLength)
{
return str.Length > maxLength ? $"{str.Substring(0, maxLength - 3)}..." : str;
if (string.IsNullOrWhiteSpace(name))
{
return "-";
}
return name.Length > maxLength ? $"{name[..(maxLength - 3)]}..." : name;
}
public static Permission MatchPermission(string str)
@ -712,15 +735,21 @@ namespace SharedLibraryCore
public static Dictionary<string, string> DictionaryFromKeyValue(this string eventLine)
{
var values = eventLine.Substring(1).Split('\\');
var values = eventLine[1..].Split('\\');
Dictionary<string, string> dict = null;
Dictionary<string, string> dict = new();
if (values.Length > 1)
if (values.Length <= 1)
{
dict = new Dictionary<string, string>();
for (var i = values.Length % 2 == 0 ? 0 : 1; i < values.Length; i += 2)
return dict;
}
for (var i = values.Length % 2 == 0 ? 0 : 1; i < values.Length; i += 2)
{
if (!dict.ContainsKey(values[i]))
{
dict.Add(values[i], values[i + 1]);
}
}
return dict;
@ -772,11 +801,6 @@ namespace SharedLibraryCore
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
}
public static void BeginGetDvar(this Server server, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginGetDvar(server.RemoteConnection, dvarName, callback, token);
}
public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default)
{
@ -807,30 +831,36 @@ namespace SharedLibraryCore
return await server.GetDvarAsync(mappedKey, defaultValue, token: token);
}
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue, CancellationToken token = default)
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue,
CancellationToken token)
{
await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
}
public static void BeginSetDvar(this Server server, string dvarName, object dvarValue,
AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginSetDvar(server.RemoteConnection, dvarName, dvarValue, callback, token);
}
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{
await SetDvarAsync(server, dvarName, dvarValue, default);
}
public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName, CancellationToken token = default)
public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName,
CancellationToken token)
{
return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token);
var response = await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token);
server.Manager.QueueEvent(new ServerCommandExecuteEvent
{
Server = server,
Source = server,
Command = commandName,
Output = response
});
return response;
}
public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName)
{
return await ExecuteCommandAsync(server, commandName, default);
return await ExecuteCommandAsync(server, commandName, default);
}
public static async Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
@ -1262,5 +1292,44 @@ namespace SharedLibraryCore
public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
public static IServiceCollection AddConfiguration<TConfigurationType>(
this IServiceCollection serviceCollection, string fileName = null, TConfigurationType defaultConfig = null)
where TConfigurationType : class
{
serviceCollection.AddSingleton(serviceProvider =>
{
var configurationHandler =
serviceProvider.GetRequiredService<IConfigurationHandlerV2<TConfigurationType>>();
var configuration =
Task.Run(() => configurationHandler.Get(fileName ?? typeof(TConfigurationType).Name, defaultConfig))
.GetAwaiter().GetResult();
if (typeof(TConfigurationType).GetInterface(nameof(IBaseConfiguration)) is not null &&
defaultConfig is null && configuration is null)
{
defaultConfig =
(TConfigurationType)((IBaseConfiguration)Activator.CreateInstance<TConfigurationType>())
.Generate();
}
if (defaultConfig is not null && configuration is null)
{
Task.Run(() => configurationHandler.Set(defaultConfig)).GetAwaiter().GetResult();
configuration = defaultConfig;
}
if (configuration is null)
{
throw new ConfigurationException(
$"Could not register configuration {typeof(TConfigurationType).Name}. Configuration file does not exist and no default configuration was provided.");
}
return configuration;
});
return serviceCollection;
}
}
}

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
using WebfrontCore.Controllers.API.Dtos;
@ -137,6 +138,16 @@ namespace WebfrontCore.Controllers.API
: HttpContext.Connection.RemoteIpAddress.ToString()
});
Manager.QueueEvent(new LoginEvent
{
Source = this,
LoginSource = LoginEvent.LoginSourceType.Webfront,
EntityId = Client.ClientId.ToString(),
Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
return Ok();
}
}
@ -165,6 +176,16 @@ namespace WebfrontCore.Controllers.API
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString()
});
Manager.QueueEvent(new LogoutEvent
{
Source = this,
LoginSource = LoginEvent.LoginSourceType.Webfront,
EntityId = Client.ClientId.ToString(),
Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

View File

@ -91,7 +91,7 @@ namespace WebfrontCore.Controllers.API
var start = DateTime.Now;
Client.CurrentServer = foundServer;
var commandEvent = new GameEvent()
var commandEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Owner = foundServer,

View File

@ -7,6 +7,7 @@ using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers;
namespace WebfrontCore.Controllers
@ -73,6 +74,16 @@ namespace WebfrontCore.Controllers
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
Manager.QueueEvent(new LoginEvent
{
Source = this,
LoginSource = LoginEvent.LoginSourceType.Webfront,
EntityId = privilegedClient.ClientId.ToString(),
Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
return Ok(Localization["WEBFRONT_ACTION_LOGIN_SUCCESS"].FormatExt(privilegedClient.CleanedName));
}
}
@ -99,6 +110,16 @@ namespace WebfrontCore.Controllers
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
Manager.QueueEvent(new LogoutEvent
{
Source = this,
LoginSource = LoginEvent.LoginSourceType.Webfront,
EntityId = Client.ClientId.ToString(),
Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

View File

@ -7,7 +7,6 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

View File

@ -1,7 +1,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@ -41,12 +40,12 @@ namespace WebfrontCore.Controllers
return NotFound();
}
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
var server = Manager.GetServers().FirstOrDefault(server => server.Id == serverId) as IGameServer;
long? matchedServerId = null;
if (server != null)
{
matchedServerId = StatManager.GetIdForServer(server);
matchedServerId = server.LegacyDatabaseId;
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);

View File

@ -3,9 +3,8 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Middleware;
@ -14,33 +13,36 @@ namespace WebfrontCore
public class Program
{
public static IManager Manager;
public static IServiceCollection Services;
public static IServiceProvider ApplicationServiceProvider;
private static IWebHost _webHost;
public static Task Init(IManager mgr, IServiceProvider existingServiceProvider, IServiceCollection services, CancellationToken cancellationToken)
public static IServiceProvider InitializeServices(Action<IServiceCollection> registerDependenciesAction, string bindUrl)
{
Services = services;
Manager = mgr;
ApplicationServiceProvider = existingServiceProvider;
var config = Manager.GetApplicationSettings().Configuration();
Manager.MiddlewareActionHandler.Register(null, new CustomCssAccentMiddlewareAction("#007ACC", "#fd7e14", config.WebfrontPrimaryColor, config.WebfrontSecondaryColor), "custom_css_accent");
return BuildWebHost().RunAsync(cancellationToken);
_webHost = BuildWebHost(registerDependenciesAction, bindUrl);
Manager = _webHost.Services.GetRequiredService<IManager>();
return _webHost.Services;
}
private static IWebHost BuildWebHost()
public static Task GetWebHostTask(CancellationToken cancellationToken)
{
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var config = _webHost.Services.GetRequiredService<ApplicationConfiguration>();
Manager.MiddlewareActionHandler.Register(null,
new CustomCssAccentMiddlewareAction("#007ACC", "#fd7e14", config.WebfrontPrimaryColor,
config.WebfrontSecondaryColor), "custom_css_accent");
return _webHost?.RunAsync(cancellationToken);
}
private static IWebHost BuildWebHost(Action<IServiceCollection> registerDependenciesAction, string bindUrl)
{
return new WebHostBuilder()
#if DEBUG
.UseContentRoot(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\..\", "WebfrontCore")))
#else
.UseContentRoot(SharedLibraryCore.Utilities.OperatingDirectory)
#endif
.UseUrls(Manager.GetApplicationSettings().Configuration().WebfrontBindUrl)
.UseUrls(bindUrl)
.UseKestrel()
.ConfigureServices(registerDependenciesAction)
.UseStartup<Startup>()
.Build();
}

View File

@ -24,6 +24,7 @@ using System.Reflection;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Client.Abstractions;
using Stats.Config;
using WebfrontCore.Controllers.API.Validation;
@ -64,23 +65,8 @@ namespace WebfrontCore
}
// Add framework services.
var mvcBuilder = services.AddMvc(_options => _options.SuppressAsyncSuffixInActionNames = false)
.AddFluentValidation()
.ConfigureApplicationPartManager(_partManager =>
{
foreach (var assembly in pluginAssemblies())
{
if (assembly.FullName.Contains("Views"))
{
_partManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly));
}
else if (assembly.FullName.Contains("Web"))
{
_partManager.ApplicationParts.Add(new AssemblyPart(assembly));
}
}
});
var mvcBuilder = services.AddMvc(options => options.SuppressAsyncSuffixInActionNames = false);
services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters();
#if DEBUG
{
@ -109,42 +95,13 @@ namespace WebfrontCore
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
});
services.AddSingleton(Program.Manager);
services.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>();
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
services.AddScoped(sp =>
Program.ApplicationServiceProvider
.GetRequiredService<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>>());
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
// todo: this needs to be handled more gracefully
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<DefaultSettings>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ILoggerFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IConfigurationHandlerFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IDatabaseContextFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IAuditInformationRepository>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ITranslationLookup>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IEnumerable<IManagerCommand>>());
#pragma warning disable CS0618
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaService>());
#pragma warning restore CS0618
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaServiceV2>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ApplicationConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>());
services.AddSingleton<IResourceQueryHelper<BanInfoRequest, BanInfo>, BanInfoResourceQueryHelper>();
services.AddSingleton(
Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>());
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<IConfigurationHandler<DefaultSettings>>());
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<IGeoLocationService>());
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<StatsConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IInteractionRegistration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IRemoteCommandService>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.