using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.RconParsers; using SharedLibraryCore; using SharedLibraryCore.Commands; using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Database; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; using SharedLibraryCore.QueryHelper; using SharedLibraryCore.Services; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using static SharedLibraryCore.GameEvent; namespace IW4MAdmin.Application { public class ApplicationManager : IManager { private readonly ConcurrentBag _servers; public List Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public ILogger Logger => GetLogger(0); public bool IsRunning { get; private set; } public bool IsInitialized { get; private set; } public DateTime StartTime { get; private set; } public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString(); public IList AdditionalRConParsers { get; } public IList AdditionalEventParsers { get; } public ITokenAuthentication TokenAuthenticator { get; } public CancellationToken CancellationToken => _tokenSource.Token; public string ExternalIPAddress { get; private set; } public bool IsRestartRequested { get; private set; } public IMiddlewareActionHandler MiddlewareActionHandler { get; } public event EventHandler OnGameEventExecuted; private readonly List _commands; private readonly ILogger _logger; private readonly List MessageTokens; private readonly ClientService ClientSvc; readonly AliasService AliasSvc; readonly PenaltyService PenaltySvc; public IConfigurationHandler ConfigHandler; readonly IPageList PageList; private readonly Dictionary _loggers = new Dictionary(); private readonly IMetaService _metaService; private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly CancellationTokenSource _tokenSource; private readonly Dictionary> _operationLookup = new Dictionary>(); private readonly ITranslationLookup _translationLookup; private readonly IConfigurationHandler _commandConfiguration; private readonly IGameServerInstanceFactory _serverInstanceFactory; private readonly IParserRegexFactory _parserRegexFactory; private readonly IEnumerable _customParserEvents; private readonly IEventHandler _eventHandler; private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IMetaRegistration _metaRegistration; private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver; public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable commands, ITranslationLookup translationLookup, IConfigurationHandler commandConfiguration, IConfigurationHandler appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable customParserEvents, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver) { MiddlewareActionHandler = actionHandler; _servers = new ConcurrentBag(); MessageTokens = new List(); ClientSvc = new ClientService(contextFactory); AliasSvc = new AliasService(); PenaltySvc = new PenaltyService(); ConfigHandler = appConfigHandler; StartTime = DateTime.UtcNow; PageList = new PageList(); AdditionalEventParsers = new List() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) }; AdditionalRConParsers = new List() { new BaseRConParser(parserRegexFactory) }; TokenAuthenticator = new TokenAuthentication(); _logger = logger; _metaService = metaService; _tokenSource = new CancellationTokenSource(); _loggers.Add(0, logger); _commands = commands.ToList(); _translationLookup = translationLookup; _commandConfiguration = commandConfiguration; _serverInstanceFactory = serverInstanceFactory; _parserRegexFactory = parserRegexFactory; _customParserEvents = customParserEvents; _eventHandler = eventHandler; _scriptCommandFactory = scriptCommandFactory; _metaRegistration = metaRegistration; _scriptPluginServiceResolver = scriptPluginServiceResolver; Plugins = plugins; } public IEnumerable Plugins { get; } public async Task ExecuteEvent(GameEvent newEvent) { #if DEBUG == true Logger.WriteDebug($"Entering event process for {newEvent.Id}"); #endif // the event has failed already if (newEvent.Failed) { goto skip; } try { await newEvent.Owner.ExecuteEvent(newEvent); // save the event info to the database var changeHistorySvc = new ChangeHistoryService(); await changeHistorySvc.Add(newEvent); #if DEBUG Logger.WriteDebug($"Processed event with id {newEvent.Id}"); #endif } catch (TaskCanceledException) { Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early"); } catch (OperationCanceledException) { Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early"); } // this happens if a plugin requires login catch (AuthorizationException ex) { newEvent.FailReason = EventFailReason.Permission; newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}"); } catch (NetworkException ex) { newEvent.FailReason = EventFailReason.Exception; Logger.WriteError(ex.Message); Logger.WriteDebug(ex.GetExceptionInfo()); } catch (ServerException ex) { newEvent.FailReason = EventFailReason.Exception; Logger.WriteWarning(ex.Message); } catch (Exception ex) { newEvent.FailReason = EventFailReason.Exception; Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner)); Logger.WriteDebug(ex.GetExceptionInfo()); } skip: // tell anyone waiting for the output that we're done newEvent.Complete(); OnGameEventExecuted?.Invoke(this, newEvent); #if DEBUG == true Logger.WriteDebug($"Exiting event process for {newEvent.Id}"); #endif } public IList GetServers() { return Servers; } public IList GetCommands() { return _commands; } public async Task UpdateServerStates() { // store the server hash code and task for it var runningUpdateTasks = new Dictionary(); while (!_tokenSource.IsCancellationRequested) { // select the server ids that have completed the update task var serverTasksToRemove = runningUpdateTasks .Where(ut => ut.Value.Status == TaskStatus.RanToCompletion || ut.Value.Status == TaskStatus.Canceled || ut.Value.Status == TaskStatus.Faulted) .Select(ut => ut.Key) .ToList(); // this is to prevent the log reader from starting before the initial // query of players on the server if (serverTasksToRemove.Count > 0) { IsInitialized = true; } // remove the update tasks as they have completd foreach (long serverId in serverTasksToRemove) { runningUpdateTasks.Remove(serverId); } // select the servers where the tasks have completed var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint))) { runningUpdateTasks.Add(server.EndPoint, Task.Run(async () => { try { await server.ProcessUpdatesAsync(_tokenSource.Token); if (server.Throttled) { await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token); } } catch (Exception e) { Logger.WriteWarning($"Failed to update status for {server}"); Logger.WriteDebug(e.GetExceptionInfo()); } finally { server.IsInitialized = true; } })); } #if DEBUG Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates"); ThreadPool.GetMaxThreads(out int workerThreads, out int n); ThreadPool.GetAvailableThreads(out int availableThreads, out int m); Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); #endif try { 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 => serverIds.Contains(s.EndPoint))) { await server.ProcessUpdatesAsync(_tokenSource.Token); } break; } } } public async Task Init() { IsRunning = true; ExternalIPAddress = await Utilities.GetExternalIP(); #region PLUGINS foreach (var plugin in Plugins) { try { if (plugin is ScriptPlugin scriptPlugin) { await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver); scriptPlugin.Watcher.Changed += async (sender, e) => { try { await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver); } catch (Exception ex) { Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name)); Logger.WriteDebug(ex.Message); } }; } else { await plugin.OnLoadAsync(this); } } catch (Exception ex) { Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}"); Logger.WriteDebug(ex.GetExceptionInfo()); } } #endregion #region CONFIG var config = ConfigHandler.Configuration(); // copy over default config if it doesn't exist if (config == null) { var defaultConfig = new BaseConfigurationHandler("DefaultSettings").Configuration(); ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate()); var newConfig = ConfigHandler.Configuration(); newConfig.AutoMessages = defaultConfig.AutoMessages; newConfig.GlobalRules = defaultConfig.GlobalRules; newConfig.Maps = defaultConfig.Maps; newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames; newConfig.QuickMessages = defaultConfig.QuickMessages; if (newConfig.Servers == null) { ConfigHandler.Set(newConfig); newConfig.Servers = new ServerConfiguration[1]; do { var serverConfig = new ServerConfiguration(); foreach (var parser in AdditionalRConParsers) { serverConfig.AddRConParser(parser); } foreach (var parser in AdditionalEventParsers) { serverConfig.AddEventParser(parser); } newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray(); } while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"])); config = newConfig; await ConfigHandler.Save(); } } else { if (string.IsNullOrEmpty(config.Id)) { config.Id = Guid.NewGuid().ToString(); await ConfigHandler.Save(); } if (string.IsNullOrEmpty(config.WebfrontBindUrl)) { config.WebfrontBindUrl = "http://0.0.0.0:1624"; await ConfigHandler.Save(); } var validator = new ApplicationConfigurationValidator(); var validationResult = validator.Validate(config); if (!validationResult.IsValid) { throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR") { Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(), ConfigurationFileName = ConfigHandler.FileName }; } foreach (var serverConfig in config.Servers) { Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig); if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null) { foreach (var parser in AdditionalRConParsers) { serverConfig.AddRConParser(parser); } foreach (var parser in AdditionalEventParsers) { serverConfig.AddEventParser(parser); } serverConfig.ModifyParsers(); } await ConfigHandler.Save(); } } if (config.Servers.Length == 0) { throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); } Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252"); #endregion #region DATABASE using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString, GetApplicationSettings().Configuration()?.DatabaseProvider)) { await new ContextSeed(db).Seed(); } #endregion #region COMMANDS if (ClientSvc.GetOwners().Result.Count > 0) { _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); } List commandsToAddToConfig = new List(); var cmdConfig = _commandConfiguration.Configuration(); if (cmdConfig == null) { cmdConfig = new CommandConfiguration(); commandsToAddToConfig.AddRange(_commands); } else { var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.CommandConfigNameForType())); commandsToAddToConfig.AddRange(unsavedCommands); } // this is because I want to store the command prefix in IW4MAdminSettings, but can't easily // inject it to all the places that need it cmdConfig.CommandPrefix = config.CommandPrefix; cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix; foreach (var cmd in commandsToAddToConfig) { cmdConfig.Commands.Add(cmd.CommandConfigNameForType(), new CommandProperties() { Name = cmd.Name, Alias = cmd.Alias, MinimumPermission = cmd.Permission, AllowImpersonation = cmd.AllowImpersonation, SupportedGames = cmd.SupportedGames }); } _commandConfiguration.Set(cmdConfig); await _commandConfiguration.Save(); #endregion _metaRegistration.Register(); #region CUSTOM_EVENTS foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) { foreach (var parser in AdditionalEventParsers) { parser.RegisterCustomEvent(customEvent.Item1, customEvent.Item2, customEvent.Item3); } } #endregion await InitializeServers(); } private async Task InitializeServers() { var config = ConfigHandler.Configuration(); int successServers = 0; Exception lastException = null; async Task Init(ServerConfiguration Conf) { try { // todo: this might not always be an IW4MServer var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer; await ServerInstance.Initialize(); _servers.Add(ServerInstance); Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname)); // add the start event for this server var e = new GameEvent() { Type = GameEvent.EventType.Start, Data = $"{ServerInstance.GameName} started", Owner = ServerInstance }; AddEvent(e); successServers++; } catch (ServerException e) { Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]")); if (e.GetType() == typeof(DvarException)) { Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}"); } lastException = e; } } await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray()); if (successServers == 0) { throw lastException; } if (successServers != config.Servers.Length) { if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"])) { throw lastException; } } } public async Task Start() => await UpdateServerStates(); public void Stop() { _tokenSource.Cancel(); IsRunning = false; } public void Restart() { IsRestartRequested = true; Stop(); } public ILogger GetLogger(long serverId) { if (_loggers.ContainsKey(serverId)) { return _loggers[serverId]; } else { var newLogger = new Logger($"IW4MAdmin-Server-{serverId}"); _loggers.Add(serverId, newLogger); return newLogger; } } public IList GetMessageTokens() { return MessageTokens; } public IList GetActiveClients() { // we're adding another to list here so we don't get a collection modified exception.. return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList(); } public ClientService GetClientService() { return ClientSvc; } public AliasService GetAliasService() { return AliasSvc; } public PenaltyService GetPenaltyService() { return PenaltySvc; } public IConfigurationHandler GetApplicationSettings() { return ConfigHandler; } public void AddEvent(GameEvent gameEvent) { _eventHandler.HandleEvent(this, gameEvent); } public IPageList GetPageList() { return PageList; } public IRConParser GenerateDynamicRConParser(string name) { return new DynamicRConParser(_parserRegexFactory) { Name = name }; } public IEventParser GenerateDynamicEventParser(string name) { return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration()) { Name = name }; } public async Task> ExecuteSharedDatabaseOperation(string operationName) { var result = await _operationLookup[operationName]; return (IList)result; } public void RegisterSharedDatabaseOperation(Task operation, string operationName) { _operationLookup.Add(operationName, operation); } public void AddAdditionalCommand(IManagerCommand command) { if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias)) { throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})"); } _commands.Add(command); } public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); } }