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 /> <Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace> <RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest> <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -16,6 +16,7 @@ using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@ -23,12 +24,18 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Context; using Data.Context;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script; using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog.Context; using Serilog.Context;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Formatting; using SharedLibraryCore.Formatting;
using SharedLibraryCore.Interfaces.Events;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger; using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@ -50,7 +57,7 @@ namespace IW4MAdmin.Application
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } = public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>(); new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; } public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token; public CancellationToken CancellationToken => _isRunningTokenSource.Token;
public string ExternalIPAddress { get; private set; } public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; } public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; } public IMiddlewareActionHandler MiddlewareActionHandler { get; }
@ -64,29 +71,30 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); 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 Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration; private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory; private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory; private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents; private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler; private readonly ICoreEventHandler _coreEventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration; private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver; private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService; private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>(); public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
@ -101,14 +109,14 @@ namespace IW4MAdmin.Application
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_tokenSource = new CancellationTokenSource(); _isRunningTokenSource = new CancellationTokenSource();
_commands = commands.ToList(); _commands = commands.ToList();
_translationLookup = translationLookup; _translationLookup = translationLookup;
_commandConfiguration = commandConfiguration; _commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory; _serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory; _parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents; _customParserEvents = customParserEvents;
_eventHandler = eventHandler; _coreEventHandler = coreEventHandler;
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver; _scriptPluginServiceResolver = scriptPluginServiceResolver;
@ -117,6 +125,8 @@ namespace IW4MAdmin.Application
_appConfig = appConfig; _appConfig = appConfig;
Plugins = plugins; Plugins = plugins;
InteractionRegistration = interactionRegistration; InteractionRegistration = interactionRegistration;
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
} }
public IEnumerable<IPlugin> Plugins { get; } public IEnumerable<IPlugin> Plugins { get; }
@ -124,7 +134,7 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
ProcessingEvents.TryAdd(newEvent.Id, newEvent); ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
@ -142,12 +152,12 @@ namespace IW4MAdmin.Application
catch (TaskCanceledException) 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) 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 // this happens if a plugin requires login
@ -186,11 +196,11 @@ namespace IW4MAdmin.Application
} }
skip: skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null) if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
{ {
var correlatedEvents = var correlatedEvents =
ProcessingEvents.Values.Where(ev => ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id) ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
.ToList(); .ToList();
await Task.WhenAll(correlatedEvents.Select(ev => await Task.WhenAll(correlatedEvents.Select(ev =>
@ -199,14 +209,16 @@ namespace IW4MAdmin.Application
foreach (var correlatedEvent in correlatedEvents) 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 // 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 // tell anyone waiting for the output that we're done
@ -226,75 +238,58 @@ namespace IW4MAdmin.Application
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList(); public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
public async Task UpdateServerStates() private Task UpdateServerStates()
{ {
// store the server hash code and task for it var index = 0;
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>(); return Task.WhenAll(_servers.Select(server =>
var timeout = TimeSpan.FromSeconds(60);
while (!_tokenSource.IsCancellationRequested) // main shutdown requested
{ {
// select the server ids that have completed the update task var thisIndex = index;
var serverTasksToRemove = runningUpdateTasks Interlocked.Increment(ref index);
.Where(ut => ut.Value.task.IsCompleted) return ProcessUpdateHandler(server, thisIndex);
.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;
}
}
} }
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); try
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{ {
_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
{ // run the final updates to clean up server
server.IsInitialized = true; await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
}
} }
public async Task Init() public async Task Init()
@ -305,18 +300,24 @@ namespace IW4MAdmin.Application
#region DATABASE #region DATABASE
_logger.LogInformation("Beginning database migration sync"); _logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]); Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token); await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token); await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
_logger.LogInformation("Finished database migration sync"); _logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]); Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion #endregion
#region EVENTS
IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
# endregion
#region PLUGINS #region PLUGINS
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
try try
{ {
if (plugin is ScriptPlugin scriptPlugin) if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver, await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>()); _serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
@ -391,13 +392,11 @@ namespace IW4MAdmin.Application
if (string.IsNullOrEmpty(_appConfig.Id)) if (string.IsNullOrEmpty(_appConfig.Id))
{ {
_appConfig.Id = Guid.NewGuid().ToString(); _appConfig.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
} }
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl)) if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{ {
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624"; _appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save();
} }
#pragma warning disable 618 #pragma warning disable 618
@ -442,8 +441,8 @@ namespace IW4MAdmin.Application
serverConfig.ModifyParsers(); serverConfig.ModifyParsers();
} }
await ConfigHandler.Save();
} }
await ConfigHandler.Save();
} }
if (_appConfig.Servers.Length == 0) if (_appConfig.Servers.Length == 0)
@ -468,7 +467,7 @@ namespace IW4MAdmin.Application
#endregion #endregion
#region COMMANDS #region COMMANDS
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token)) if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -526,7 +525,7 @@ namespace IW4MAdmin.Application
} }
} }
#endregion #endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]); Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
IsInitialized = true; IsInitialized = true;
@ -543,26 +542,23 @@ namespace IW4MAdmin.Application
try try
{ {
// todo: this might not always be an IW4MServer // todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer; var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
using (LogContext.PushProperty("Server", ServerInstance.ToString())) using (LogContext.PushProperty("Server", serverInstance!.ToString()))
{ {
_logger.LogInformation("Beginning server communication initialization"); _logger.LogInformation("Beginning server communication initialization");
await ServerInstance.Initialize(); await serverInstance.Initialize();
_servers.Add(ServerInstance); _servers.Add(serverInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors())); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname); _logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname);
} }
// add the start event for this server QueueEvent(new MonitorStartEvent
var e = new GameEvent()
{ {
Type = EventType.Start, Server = serverInstance,
Data = $"{ServerInstance.GameName} started", Source = this
Owner = ServerInstance });
};
AddEvent(e);
successServers++; 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() public async Task Stop()
{ {
foreach (var plugin in Plugins) foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
{ {
try try
{ {
@ -607,19 +619,32 @@ namespace IW4MAdmin.Application
{ {
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name); _logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
} }
} }
_tokenSource.Cancel(); _isRunningTokenSource.Cancel();
IsRunning = false; IsRunning = false;
} }
public void Restart() public async Task Restart()
{ {
IsRestartRequested = true; IsRestartRequested = true;
Stop().GetAwaiter().GetResult(); await Stop();
_tokenSource.Dispose();
_tokenSource = new CancellationTokenSource(); using var subscriptionTimeoutToken = new CancellationTokenSource();
subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
IGameEventSubscriptions.ClearEventInvocations();
IGameServerEventSubscriptions.ClearEventInvocations();
IManagementEventSubscriptions.ClearEventInvocations();
_isRunningTokenSource.Dispose();
_isRunningTokenSource = new CancellationTokenSource();
_eventHandlerTokenSource.Dispose();
_eventHandlerTokenSource = new CancellationTokenSource();
} }
[Obsolete] [Obsolete]
@ -661,9 +686,14 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent) 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() public IPageList GetPageList()
{ {
return PageList; return PageList;
@ -698,15 +728,132 @@ namespace IW4MAdmin.Application
public void AddAdditionalCommand(IManagerCommand command) 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 void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager; 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 SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -31,6 +31,9 @@ using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts; using SharedLibraryCore.Alerts;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Interfaces.Events;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -44,11 +47,12 @@ namespace IW4MAdmin
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private long lastGameTime = 0; private long lastGameTime = 0;
public int Id { get; private set; }
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter; private readonly IClientNoticeMessageFormatter _messageFormatter;
private readonly ILookupCache<EFServer> _serverCache; private readonly ILookupCache<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration; private readonly CommandConfiguration _commandConfiguration;
private EFServer _cachedDatabaseServer;
private readonly StatManager _statManager;
public IW4MServer( public IW4MServer(
ServerConfiguration serverConfiguration, ServerConfiguration serverConfiguration,
@ -72,6 +76,18 @@ namespace IW4MAdmin
_messageFormatter = messageFormatter; _messageFormatter = messageFormatter;
_serverCache = serverCache; _serverCache = serverCache;
_commandConfiguration = commandConfiguration; _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) public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -108,7 +124,7 @@ namespace IW4MAdmin
Clients[client.ClientNumber] = client; Clients[client.ClientNumber] = client;
ServerLogger.LogDebug("End PreConnect for {client}", client.ToString()); ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
var e = new GameEvent() var e = new GameEvent
{ {
Origin = client, Origin = client,
Owner = this, Owner = this,
@ -116,6 +132,11 @@ namespace IW4MAdmin
}; };
Manager.AddEvent(e); Manager.AddEvent(e);
Manager.QueueEvent(new ClientStateInitializeEvent
{
Client = client,
Source = this,
});
return client; return client;
} }
@ -210,10 +231,17 @@ namespace IW4MAdmin
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString()); E.Origin.ToString());
await cmd.ExecuteAsync(E); await cmd.ExecuteAsync(E);
Manager.QueueEvent(new ClientExecuteCommandEvent
{
Command = cmd,
Client = E.Origin,
Source = this,
CommandText = E.Data
});
} }
var pluginTasks = Manager.Plugins var pluginTasks = Manager.Plugins.Where(plugin => !plugin.IsParser)
.Select(async plugin => await CreatePluginTask(plugin, E)); .Select(plugin => CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks); await Task.WhenAll(pluginTasks);
} }
@ -250,7 +278,7 @@ namespace IW4MAdmin
try try
{ {
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token); await plugin.OnEventAsync(gameEvent, this);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -277,29 +305,7 @@ namespace IW4MAdmin
{ {
ServerLogger.LogDebug("processing event of type {type}", E.Type); ServerLogger.LogDebug("processing event of type {type}", E.Type);
if (E.Type == GameEvent.EventType.Start) if (E.Type == GameEvent.EventType.ConnectionLost)
{
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)
{ {
var exception = E.Extra as Exception; var exception = E.Extra as Exception;
ServerLogger.LogError(exception, ServerLogger.LogError(exception,
@ -350,9 +356,18 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.ChangePermission) else if (E.Type == GameEvent.EventType.ChangePermission)
{ {
var newPermission = (Permission) E.Extra; var newPermission = (Permission) E.Extra;
var oldPermission = E.Target.Level;
ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}", ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
E.Origin.ToString(), E.Target.ToString(), newPermission); E.Origin.ToString(), E.Target.ToString(), newPermission);
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin); 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) else if (E.Type == GameEvent.EventType.Connect)
@ -500,6 +515,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty); await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin); E.Target.SetLevel(Permission.Flagged, E.Origin);
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = E.Target,
Penalty = newPenalty
});
} }
else if (E.Type == GameEvent.EventType.Unflag) else if (E.Type == GameEvent.EventType.Unflag)
@ -519,6 +540,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId, await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.GameName, E.Target.CurrentAlias?.IPAddress); E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty); await Manager.GetPenaltyService().Create(unflagPenalty);
Manager.QueueEvent(new ClientPenaltyRevokeEvent
{
Client = E.Target,
Penalty = unflagPenalty
});
} }
else if (E.Type == GameEvent.EventType.Report) else if (E.Type == GameEvent.EventType.Report)
@ -554,6 +581,13 @@ namespace IW4MAdmin
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"] Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner)); .FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
} }
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = E.Target,
Penalty = newReport,
Source = this
});
} }
else if (E.Type == GameEvent.EventType.TempBan) else if (E.Type == GameEvent.EventType.TempBan)
@ -728,6 +762,11 @@ namespace IW4MAdmin
{ {
MaxClients = int.Parse(dict["com_maxclients"]); MaxClients = int.Parse(dict["com_maxclients"]);
} }
else if (dict.ContainsKey("com_maxplayers"))
{
MaxClients = int.Parse(dict["com_maxplayers"]);
}
if (dict.ContainsKey("mapname")) if (dict.ContainsKey("mapname"))
{ {
@ -772,34 +811,6 @@ namespace IW4MAdmin
{ {
E.Origin.UpdateTeam(E.Extra as string); 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) lock (ChatHistory)
{ {
@ -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) private async Task OnClientUpdate(EFClient origin)
{ {
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId); var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
@ -909,22 +967,15 @@ namespace IW4MAdmin
public override async Task<long> GetIdForServer(Server server = null) public override async Task<long> GetIdForServer(Server server = null)
{ {
server ??= this; server ??= this;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
}
// todo: this is not stable and will need to be migrated again... return (await _serverCache.FirstAsync(cachedServer =>
long id = HashCode.Combine(server.IP, server.Port); cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
id = id < 0 ? Math.Abs(id) : id; }
var serverId = (await _serverCache private long BuildLegacyDatabaseId()
.FirstAsync(_server => _server.ServerId == server.EndPoint || {
_server.EndPoint == server.ToString() || long id = HashCode.Combine(ListenAddress, ListenPort);
_server.ServerId == id))?.ServerId; return id < 0 ? Math.Abs(id) : id;
return !serverId.HasValue ? id : serverId.Value;
} }
private void UpdateMap(string mapname) private void UpdateMap(string mapname)
@ -983,7 +1034,7 @@ namespace IW4MAdmin
{ {
await client.OnDisconnect(); await client.OnDisconnect();
var e = new GameEvent() var e = new GameEvent
{ {
Type = GameEvent.EventType.Disconnect, Type = GameEvent.EventType.Disconnect,
Owner = this, Owner = this,
@ -994,6 +1045,14 @@ namespace IW4MAdmin
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token); 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; private DateTime _lastMessageSent = DateTime.Now;
@ -1075,6 +1134,16 @@ namespace IW4MAdmin
Manager.AddEvent(gameEvent); Manager.AddEvent(gameEvent);
} }
if (polledClients[2].Any())
{
Manager.QueueEvent(new ClientDataUpdateEvent
{
Clients = new ReadOnlyCollection<EFClient>(polledClients[2]),
Server = this,
Source = this,
});
}
if (Throttled) if (Throttled)
{ {
var gameEvent = new GameEvent var gameEvent = new GameEvent
@ -1086,6 +1155,12 @@ namespace IW4MAdmin
}; };
Manager.AddEvent(gameEvent); Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionRestoreEvent
{
Server = this,
Source = this
});
} }
LastPoll = DateTime.Now; LastPoll = DateTime.Now;
@ -1109,6 +1184,12 @@ namespace IW4MAdmin
}; };
Manager.AddEvent(gameEvent); Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionInterruptEvent
{
Server = this,
Source = this
});
return true; return true;
} }
finally finally
@ -1469,6 +1550,12 @@ namespace IW4MAdmin
.FormatExt(activeClient.Warnings, activeClient.Name, reason); .FormatExt(activeClient.Warnings, activeClient.Name, reason);
activeClient.CurrentServer.Broadcast(message); 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) 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()); ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick); 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) 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()); ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick); 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) 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)); _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString); await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
} }
Manager.QueueEvent(new ClientPenaltyEvent
{
Client = targetClient,
Penalty = newPenalty
});
} }
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient) public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
@ -1596,6 +1701,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId, await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress); targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty); await Manager.GetPenaltyService().Create(unbanPenalty);
Manager.QueueEvent(new ClientPenaltyRevokeEvent
{
Client = targetClient,
Penalty = unbanPenalty
});
} }
public override void InitializeTokens() 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("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(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()); public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager; private static ApplicationManager _serverManager;
private static Task _applicationTask; private static Task _applicationTask;
private static ServiceProvider _serviceProvider; private static IServiceProvider _serviceProvider;
/// <summary> /// <summary>
/// entrypoint of the application /// entrypoint of the application
@ -112,23 +112,24 @@ namespace IW4MAdmin.Application
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322(); ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services..."); 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 configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
var tasks = new[] await configHandler.BuildAsync();
{ _serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
versionChecker.CheckVersion(), (configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
_applicationTask
}; _serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
await _serverManager.Init(); 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) catch (Exception e)
@ -178,21 +179,20 @@ namespace IW4MAdmin.Application
{ {
goto restart; goto restart;
} }
await _serviceProvider.DisposeAsync();
} }
/// <summary> /// <summary>
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services) private static Task RunApplicationTasksAsync(ILogger logger, IServiceProvider serviceProvider)
{ {
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken) ? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken)
: Task.CompletedTask; : 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, // we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting // because we can't exit early from waiting on console input, and it prevents us from restarting
@ -203,18 +203,15 @@ namespace IW4MAdmin.Application
var tasks = new[] var tasks = new[]
{ {
versionChecker.CheckVersion(),
webfrontTask, webfrontTask,
_serverManager.Start(), serviceProvider.GetRequiredService<IMasterCommunication>()
_serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken), .RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken) collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
}; };
logger.LogDebug("Starting webfront and input tasks"); logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks); return Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully");
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
} }
/// <summary> /// <summary>
@ -302,8 +299,21 @@ namespace IW4MAdmin.Application
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations(); var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins) foreach (var pluginType in plugins)
{ {
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName); var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
try
{
var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
registrationMethod?.Invoke(null, new object[] { serviceCollection });
}
catch (Exception ex)
{
defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
}
} }
// register the plugin commands // register the plugin commands
@ -351,13 +361,11 @@ namespace IW4MAdmin.Application
/// <summary> /// <summary>
/// Configures the dependency injection services /// Configures the dependency injection services
/// </summary> /// </summary>
private static async Task<IServiceCollection> ConfigureServices(string[] args) private static void ConfigureServices(IServiceCollection serviceCollection)
{ {
// todo: this is a quick fix // todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings") serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>() .AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>() .AddConfiguration<CommandConfiguration>()
@ -365,14 +373,10 @@ namespace IW4MAdmin.Application
// for legacy purposes. update at some point // for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await appConfigHandler.BuildAsync(); appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultConfigHandler.BuildAsync();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration"); var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
await commandConfigHandler.BuildAsync(); commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
await statsCommandHandler.BuildAsync();
var defaultConfig = defaultConfigHandler.Configuration();
var appConfig = appConfigHandler.Configuration(); var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080") ? new Uri("http://127.0.0.1:8080")
@ -385,13 +389,6 @@ namespace IW4MAdmin.Application
var masterRestClient = RestClient.For<IMasterApi>(httpClient); var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig); 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 // register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames) foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{ {
@ -402,17 +399,10 @@ namespace IW4MAdmin.Application
} }
// build the dependency list // build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection serviceCollection
.AddBaseLogger(appConfig) .AddBaseLogger(appConfig)
.AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler) .AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler) .AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig)
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider => .AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>() serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration()) .Configuration() ?? new CommandConfiguration())
@ -464,7 +454,9 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataCollector, ServerDataCollector>() .AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>() .AddSingleton<IAlertManager, AlertManager>()
#pragma warning disable CS0618
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
#pragma warning restore CS0618
.AddSingleton<IInteractionRegistration, InteractionRegistration>() .AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>() .AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(new ConfigurationWatcher()) .AddSingleton(new ConfigurationWatcher())
@ -472,19 +464,10 @@ namespace IW4MAdmin.Application
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>() .AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents")) serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
serviceCollection.AddSource(); serviceCollection.AddSource();
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
return serviceCollection;
} }
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig) 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -27,7 +28,7 @@ public class RemoteCommandService : IRemoteCommandService
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command, public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server) 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; return result;
} }
@ -56,7 +57,8 @@ public class RemoteCommandService : IRemoteCommandService
: $"{_appConfig.CommandPrefix}{command}", : $"{_appConfig.CommandPrefix}{command}",
Origin = client, Origin = client,
Owner = server, Owner = server,
IsRemote = true IsRemote = true,
CorrelationId = Guid.NewGuid()
}; };
server.Manager.AddEvent(remoteEvent); server.Manager.AddEvent(remoteEvent);
@ -72,7 +74,7 @@ public class RemoteCommandService : IRemoteCommandService
{ {
response = new[] response = new[]
{ {
new CommandResponseInfo() new CommandResponseInfo
{ {
ClientId = client.ClientId, ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"] Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
@ -90,7 +92,7 @@ public class RemoteCommandService : IRemoteCommandService
} }
} }
catch (System.OperationCanceledException) catch (OperationCanceledException)
{ {
response = new[] response = new[]
{ {

View File

@ -12,8 +12,10 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Events.Management;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -24,28 +26,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IManager _manager; private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly IEventPublisher _eventPublisher;
private bool _inProgress; private bool _inProgress;
private TimeSpan _period; private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig, public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher) IManager manager, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_appConfig = appConfig; _appConfig = appConfig;
_manager = manager; _manager = manager;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_eventPublisher = eventPublisher;
_eventPublisher.OnClientConnect += SaveConnectionInfo; IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
_eventPublisher.OnClientDisconnect += SaveConnectionInfo; IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
}
~ServerDataCollector()
{
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
} }
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default) public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
@ -131,18 +125,19 @@ namespace IW4MAdmin.Application.Misc
await context.SaveChangesAsync(token); 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 context.ConnectionHistory.Add(new EFClientConnectionHistory
{ {
ClientId = gameEvent.Origin.ClientId, ClientId = stateEvent.Client.ClientId,
ServerId = gameEvent.Owner.GetIdForServer().Result, ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect ConnectionType = stateEvent is ClientStateAuthorizeEvent
? Reference.ConnectionType.Connect ? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect : 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.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null) .Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken); .CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan); }, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try try
{ {

View File

@ -151,10 +151,12 @@ public class ScriptPluginV2 : IPluginV2
} }
ScriptEngine.Execute(pluginScript); ScriptEngine.Execute(pluginScript);
#pragma warning disable CS8974
var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper), var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper),
JsValue.FromObject(ScriptEngine, _pluginServiceResolver), JsValue.FromObject(ScriptEngine, _pluginServiceResolver),
JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper), JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper),
JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this))); JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this)));
#pragma warning restore CS8974
if (initResult.IsNull() || initResult.IsUndefined()) 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, private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
string? ipAddress) string ipAddress)
{ {
return group => return group =>
{ {
ClientResourceResponse? match = null; ClientResourceResponse match = null;
var lowercaseClientName = clientName?.ToLower(); var lowercaseClientName = clientName?.ToLower();
if (!string.IsNullOrWhiteSpace(lowercaseClientName)) if (!string.IsNullOrWhiteSpace(lowercaseClientName))

View File

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

View File

@ -59,7 +59,9 @@ public class Plugin : IPluginV2
return true; 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) if (muteMeta.MuteState is not MuteState.Muted)
{ {
return true; return true;

View File

@ -196,7 +196,7 @@ const plugin = {
let data = []; let data = [];
const metaService = this.serviceResolver.ResolveService('IMetaServiceV2'); const metaService = this.serviceResolver.resolveService('IMetaServiceV2');
if (event.subType === 'Meta') { if (event.subType === 'Meta') {
const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result; const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result;
@ -237,8 +237,8 @@ const plugin = {
this.logger.logDebug('ClientId={clientId}', clientId); this.logger.logDebug('ClientId={clientId}', clientId);
if (clientId == null) { if (clientId == null || isNaN(clientId)) {
this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType); 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', { this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
ClientNumber: event.clientNumber ClientNumber: event.clientNumber
}, undefined, { }, 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 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 Microsoft.Extensions.Logging;
using Serilog.Context; using Serilog.Context;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
public class GameEvent public class GameEvent : CoreEvent
{ {
public enum EventFailReason public enum EventFailReason
{ {
@ -133,6 +134,8 @@ namespace SharedLibraryCore
/// connection was restored to a server (the server began responding again) /// connection was restored to a server (the server began responding again)
/// </summary> /// </summary>
ConnectionRestored, ConnectionRestored,
SayTeam = 99,
// events "generated" by clients // events "generated" by clients
/// <summary> /// <summary>
@ -246,7 +249,7 @@ namespace SharedLibraryCore
/// team info printed out by game script /// team info printed out by game script
/// </summary> /// </summary>
JoinTeam = 304, JoinTeam = 304,
/// <summary> /// <summary>
/// used for community generated plugin events /// used for community generated plugin events
/// </summary> /// </summary>
@ -267,7 +270,7 @@ namespace SharedLibraryCore
public GameEvent() public GameEvent()
{ {
Time = DateTime.UtcNow; Time = DateTime.UtcNow;
Id = GetNextEventId(); IncrementalId = GetNextEventId();
} }
~GameEvent() ~GameEvent()
@ -275,8 +278,6 @@ namespace SharedLibraryCore
_eventFinishedWaiter.Dispose(); _eventFinishedWaiter.Dispose();
} }
public EventSource Source { get; set; }
/// <summary> /// <summary>
/// suptype of the event for more detailed classification /// suptype of the event for more detailed classification
/// </summary> /// </summary>
@ -293,11 +294,10 @@ namespace SharedLibraryCore
public bool IsRemote { get; set; } public bool IsRemote { get; set; }
public object Extra { get; set; } public object Extra { get; set; }
public DateTime Time { get; set; } public DateTime Time { get; set; }
public long Id { get; } public long IncrementalId { get; }
public EventFailReason FailReason { get; set; } public EventFailReason FailReason { get; set; }
public bool Failed => FailReason != EventFailReason.None; public bool Failed => FailReason != EventFailReason.None;
public Guid CorrelationId { get; set; } = Guid.NewGuid(); public List<string> Output { get; set; } = new();
public List<string> Output { get; set; } = new List<string>();
/// <summary> /// <summary>
/// Indicates if the event should block until it is complete /// Indicates if the event should block until it is complete
@ -328,23 +328,31 @@ namespace SharedLibraryCore
/// <returns>waitable task </returns> /// <returns>waitable task </returns>
public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token) public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token)
{ {
var processed = false; if (FailReason == EventFailReason.Timeout)
Utilities.DefaultLogger.LogDebug("Begin wait for event {Id}", Id);
try
{ {
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; processed = true;
} }
catch (OperationCanceledException)
{
processed = false;
}
finally 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;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
using SharedLibraryCore.Database.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> /// <param name="previousPenalty">previous penalty the kick is occuring for (if applicable)</param>
/// <returns></returns> /// <returns></returns>
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null); Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
/// <summary>
/// Time the most recent match ended
/// </summary>
DateTime? MatchEndTime { get; } DateTime? MatchEndTime { get; }
/// <summary>
/// Time the current match started
/// </summary>
DateTime? MatchStartTime { get; } 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 System.Threading.Tasks;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
@ -34,7 +35,7 @@ namespace SharedLibraryCore.Interfaces
Task Init(); Task Init();
Task Start(); Task Start();
Task Stop(); Task Stop();
void Restart(); Task Restart();
[Obsolete] [Obsolete]
ILogger GetLogger(long serverId); ILogger GetLogger(long serverId);
@ -87,6 +88,11 @@ namespace SharedLibraryCore.Interfaces
/// <param name="gameEvent">event to be processed</param> /// <param name="gameEvent">event to be processed</param>
void AddEvent(GameEvent gameEvent); void AddEvent(GameEvent gameEvent);
/// <summary>
/// queues an event for processing
/// </summary>
void QueueEvent(CoreEvent coreEvent);
/// <summary> /// <summary>
/// adds an additional (script) command to the command list /// adds an additional (script) command to the command list
/// </summary> /// </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

@ -55,8 +55,6 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default); 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> /// <summary>
/// set value of DVAR by name /// set value of DVAR by name
@ -67,9 +65,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default); 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> /// <summary>
/// executes a console command on the server /// executes a console command on the server
/// </summary> /// </summary>

View File

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

View File

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

View File

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

View File

@ -14,10 +14,13 @@ using System.Threading.Tasks;
using Data.Models; using Data.Models;
using Humanizer; using Humanizer;
using Humanizer.Localisation; using Humanizer.Localisation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta; using SharedLibraryCore.Dtos.Meta;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization; using SharedLibraryCore.Localization;
@ -26,7 +29,6 @@ using static SharedLibraryCore.Server;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
using static Data.Models.EFPenalty; using static Data.Models.EFPenalty;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using RegionInfo = System.Globalization.RegionInfo;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
@ -43,7 +45,7 @@ namespace SharedLibraryCore
public static Encoding EncodingType; public static Encoding EncodingType;
public static Layout CurrentLocalization = new Layout(new Dictionary<string, string>()); 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[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!'; 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> /// <summary>
/// fallback id for world events /// fallback id for world events
/// </summary> /// </summary>
@ -95,14 +113,19 @@ namespace SharedLibraryCore
/// <summary> /// <summary>
/// caps client name to the specified character length - 3 /// 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> /// </summary>
/// <param name="str">client name</param> /// <param name="name">client name</param>
/// <param name="maxLength">max number of characters for the name</param> /// <param name="maxLength">max number of characters for the name</param>
/// <returns></returns> /// <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) public static Permission MatchPermission(string str)
@ -712,15 +735,21 @@ namespace SharedLibraryCore
public static Dictionary<string, string> DictionaryFromKeyValue(this string eventLine) 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>(); return dict;
for (var i = values.Length % 2 == 0 ? 0 : 1; i < values.Length; i += 2) }
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]); dict.Add(values[i], values[i + 1]);
}
} }
return dict; return dict;
@ -771,11 +800,6 @@ namespace SharedLibraryCore
{ {
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token); 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, public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default) T fallbackValue = default)
@ -807,30 +831,36 @@ namespace SharedLibraryCore
return await server.GetDvarAsync(mappedKey, defaultValue, token: token); 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); 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) public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{ {
await SetDvarAsync(server, dvarName, dvarValue, default); 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) 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) 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("", public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray()); 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.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using WebfrontCore.Controllers.API.Dtos; using WebfrontCore.Controllers.API.Dtos;
@ -136,6 +137,16 @@ namespace WebfrontCore.Controllers.API
? HttpContext.Request.Headers["X-Forwarded-For"].ToString() ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString() : 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(); return Ok();
} }
@ -165,6 +176,16 @@ namespace WebfrontCore.Controllers.API
? HttpContext.Request.Headers["X-Forwarded-For"].ToString() ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.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); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

View File

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

View File

@ -47,4 +47,4 @@ namespace WebfrontCore.Controllers
return View(info); return View(info);
} }
} }
} }

View File

@ -7,6 +7,7 @@ using System;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
@ -72,6 +73,16 @@ namespace WebfrontCore.Controllers
? HttpContext.Request.Headers["X-Forwarded-For"].ToString() ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString() : 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)); 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.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.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); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

View File

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

View File

@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
@ -41,12 +40,12 @@ namespace WebfrontCore.Controllers
return NotFound(); 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; long? matchedServerId = null;
if (server != null) if (server != null)
{ {
matchedServerId = StatManager.GetIdForServer(server); matchedServerId = server.LegacyDatabaseId;
} }
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token); hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);

View File

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

View File

@ -24,6 +24,7 @@ using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Config; using Stats.Config;
using WebfrontCore.Controllers.API.Validation; using WebfrontCore.Controllers.API.Validation;
@ -64,23 +65,8 @@ namespace WebfrontCore
} }
// Add framework services. // Add framework services.
var mvcBuilder = services.AddMvc(_options => _options.SuppressAsyncSuffixInActionNames = false) var mvcBuilder = services.AddMvc(options => options.SuppressAsyncSuffixInActionNames = false);
.AddFluentValidation() services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters();
.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));
}
}
});
#if DEBUG #if DEBUG
{ {
@ -109,42 +95,13 @@ namespace WebfrontCore
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn; options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
}); });
services.AddSingleton(Program.Manager);
services.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>(); services.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>();
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>(); services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>(); services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>(); services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>(); services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
services.AddScoped(sp =>
Program.ApplicationServiceProvider
.GetRequiredService<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>>());
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>)); services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
// todo: this needs to be handled more gracefully services.AddSingleton<IResourceQueryHelper<BanInfoRequest, BanInfo>, BanInfoResourceQueryHelper>();
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. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.