fix for runaway regular expression on linux

explicitly set string dvars in quotes to allow setting empty dvars
allow piping in input from command line (#114)
update the distribution for top stats elo
prevent game log file rotation from stopping event parsing
This commit is contained in:
RaidMax 2020-04-01 14:11:56 -05:00
parent 02a784ad09
commit 9fdf4bad9c
35 changed files with 504 additions and 124 deletions

1
.gitignore vendored
View File

@ -241,3 +241,4 @@ launchSettings.json
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles
/Tests/ApplicationTests/Files/GameEvents.json

View File

@ -25,11 +25,11 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
<PackageReference Include="RestEase" Version="1.4.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
</ItemGroup>

View File

@ -59,11 +59,12 @@ namespace IW4MAdmin.Application
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins)
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -74,8 +75,8 @@ namespace IW4MAdmin.Application
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser() };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser() };
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_metaService = new MetaService();
_tokenSource = new CancellationTokenSource();
@ -84,6 +85,7 @@ namespace IW4MAdmin.Application
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
Plugins = plugins;
}
@ -771,7 +773,7 @@ namespace IW4MAdmin.Application
public IRConParser GenerateDynamicRConParser(string name)
{
return new DynamicRConParser()
return new DynamicRConParser(_parserRegexFactory)
{
Name = name
};
@ -779,7 +781,7 @@ namespace IW4MAdmin.Application
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser()
return new DynamicEventParser(_parserRegexFactory)
{
Name = name
};

View File

@ -2,18 +2,16 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
public class BaseEventParser : IEventParser
{
public BaseEventParser()
public BaseEventParser(IParserRegexFactory parserRegexFactory)
{
Configuration = new DynamicEventParserConfiguration()
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
GameDirectory = "main",
};
@ -66,6 +64,8 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
}
public IEventParserConfiguration Configuration { get; set; }
@ -80,44 +80,48 @@ namespace IW4MAdmin.Application.EventParsers
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Regex.Match(logLine, @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )");
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
int gameTime = 0;
if (timeMatch.Success)
{
gameTime = (timeMatch.Groups.Values as IEnumerable<object>)
gameTime = timeMatch
.Values
.Skip(2)
.Select(_value => int.Parse(_value.ToString()))
// this converts the timestamp into seconds passed
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
logLine = logLine.Substring(timeMatch.Value.Length);
}
// we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length);
}
string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
if (eventType == "say" || eventType == "sayteam")
{
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success)
{
string message = matchResult
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
long originId = matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle);
long originId = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle);
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
// todo: these need to defined outside of here
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
@ -129,7 +133,7 @@ namespace IW4MAdmin.Application.EventParsers
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
@ -141,19 +145,21 @@ namespace IW4MAdmin.Application.EventParsers
if (eventType == "K")
{
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
long originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long originId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime
};
@ -162,19 +168,21 @@ namespace IW4MAdmin.Application.EventParsers
if (eventType == "D")
{
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (regexMatch.Success)
if (match.Success)
{
long originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long originId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime
};
@ -183,9 +191,9 @@ namespace IW4MAdmin.Application.EventParsers
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (regexMatch.Success)
if (match.Success)
{
return new GameEvent()
{
@ -195,10 +203,10 @@ namespace IW4MAdmin.Application.EventParsers
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
},
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
NetworkId = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
@ -210,8 +218,9 @@ namespace IW4MAdmin.Application.EventParsers
if (eventType == "Q")
{
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
if (regexMatch.Success)
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
return new GameEvent()
{
@ -221,10 +230,10 @@ namespace IW4MAdmin.Application.EventParsers
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
},
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
NetworkId = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
@ -279,7 +288,6 @@ namespace IW4MAdmin.Application.EventParsers
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using static SharedLibraryCore.Server;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.EventParsers
{
@ -11,5 +8,8 @@ namespace IW4MAdmin.Application.EventParsers
/// </summary>
sealed internal class DynamicEventParser : BaseEventParser
{
public DynamicEventParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
{
}
}
}

View File

@ -10,12 +10,24 @@ namespace IW4MAdmin.Application.EventParsers
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; } = new ParserRegex();
public ParserRegex Join { get; set; } = new ParserRegex();
public ParserRegex Quit { get; set; } = new ParserRegex();
public ParserRegex Kill { get; set; } = new ParserRegex();
public ParserRegex Damage { get; set; } = new ParserRegex();
public ParserRegex Action { get; set; } = new ParserRegex();
public ParserRegex Say { get; set; }
public ParserRegex Join { get; set; }
public ParserRegex Quit { get; set; }
public ParserRegex Kill { get; set; }
public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
{
Say = parserRegexFactory.CreateParserRegex();
Join = parserRegexFactory.CreateParserRegex();
Quit = parserRegexFactory.CreateParserRegex();
Kill = parserRegexFactory.CreateParserRegex();
Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex();
}
}
}

View File

@ -0,0 +1,35 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// implementation of the IParserPatternMatcher for windows (really it's the only implementation)
/// </summary>
public class ParserPatternMatcher : IParserPatternMatcher
{
private Regex regex;
/// <inheritdoc/>
public void Compile(string pattern)
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
/// <inheritdoc/>
public IMatchResult Match(string input)
{
var match = regex.Match(input);
return new ParserMatchResult()
{
Success = match.Success,
Values = (match.Groups as IEnumerable<object>)?
.Select(_item => _item.ToString()).ToArray() ?? new string[0]
};
}
}
}

View File

@ -0,0 +1,26 @@
using SharedLibraryCore.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// Implementation of the IParserRegexFactory
/// </summary>
public class ParserRegexFactory : IParserRegexFactory
{
private readonly IServiceProvider _serviceProvider;
/// <inheritdoc/>
public ParserRegexFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public ParserRegex CreateParserRegex()
{
return new ParserRegex(_serviceProvider.GetService<IParserPatternMatcher>());
}
}
}

View File

@ -6,12 +6,11 @@ using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
public class GameLogEventDetection
{
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly string _gameLogFile;
private readonly bool _ignoreBots;
class EventState
@ -20,12 +19,13 @@ namespace IW4MAdmin.Application.IO
public string ServerId { get; set; }
}
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri, IGameLogReader reader = null)
{
_gameLogFile = gameLogPath;
_reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : _reader = new GameLogReader(gameLogPath, server.EventParser);
_reader = gameLogServerUri != null
? reader ?? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser)
: reader ?? new GameLogReader(gameLogPath, server.EventParser);
_server = server;
_ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
_ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
}
public async Task PollForChanges()
@ -52,7 +52,7 @@ namespace IW4MAdmin.Application.IO
_server.Logger.WriteDebug("Stopped polling for changes");
}
private async Task UpdateLogEvents()
public async Task UpdateLogEvents()
{
long fileSize = _reader.Length;
@ -65,7 +65,10 @@ namespace IW4MAdmin.Application.IO
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
{
previousFileSize = fileSize;
return;
}
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize);

View File

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.IO
_parser = parser;
}
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();

View File

@ -16,27 +16,27 @@ namespace IW4MAdmin.Application.IO
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
readonly IEventParser Parser;
readonly IGameLogServer Api;
private readonly IEventParser _eventParser;
private readonly IGameLogServer _logServerApi;
readonly string logPath;
private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
{
this.logPath = logPath.ToBase64UrlSafeString(); ;
Parser = parser;
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
this.logPath = logPath.ToBase64UrlSafeString();
_eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUri);
}
public long Length => -1;
public int UpdateInterval => 500;
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
var events = new List<GameEvent>();
string b64Path = logPath;
var response = await Api.Log(b64Path, lastKey);
var response = await _logServerApi.Log(b64Path, lastKey);
lastKey = response.NextKey;
if (!response.Success && string.IsNullOrEmpty(lastKey))
@ -48,17 +48,17 @@ namespace IW4MAdmin.Application.IO
else if (!string.IsNullOrWhiteSpace(response.Data))
{
// parse each line
foreach (string eventLine in response.Data
.Split(Environment.NewLine)
.Where(_line => _line.Length > 0))
var lines = response.Data
.Split(Environment.NewLine)
.Where(_line => _line.Length > 0);
foreach (string eventLine in lines)
{
try
{
var gameEvent = Parser.GenerateGameEvent(eventLine);
// this trim end should hopefully fix the nasty runaway regex
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
events.Add(gameEvent);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {gameEvent.Id} from http");
#endif
}
catch (Exception e)

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin
public class IW4MServer : Server
{
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEventDetection LogEvent;
public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup;
private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0;
@ -891,8 +891,8 @@ namespace IW4MAdmin
EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
RconParser = RconParser ?? new BaseRConParser();
EventParser = EventParser ?? new BaseEventParser();
RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RemoteConnection.SetConfiguration(RconParser.Configuration);
@ -949,6 +949,14 @@ namespace IW4MAdmin
try
{
var website = await this.GetDvarAsync<string>("_website");
// this occurs for games that don't give us anything back when
// the dvar is not set
if (string.IsNullOrWhiteSpace(website.Value))
{
throw new DvarException("value is empty");
}
Website = website.Value;
}

View File

@ -1,6 +1,6 @@
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
@ -87,7 +87,7 @@ namespace IW4MAdmin.Application
catch (Exception e)
{
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
string exitMessage = translationLookup == null ? "Press any key to exit..." : translationLookup["MANAGER_EXIT"];
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
Console.WriteLine(failMessage);
@ -115,7 +115,7 @@ namespace IW4MAdmin.Application
}
Console.WriteLine(exitMessage);
Console.ReadKey();
await Console.In.ReadAsync(new char[1], 0, 1);
return;
}
@ -237,7 +237,7 @@ namespace IW4MAdmin.Application
{
while (!ServerManager.CancellationToken.IsCancellationRequested)
{
lastCommand = Console.ReadLine();
lastCommand = await Console.In.ReadLineAsync();
if (lastCommand?.Length > 0)
{
@ -282,6 +282,8 @@ namespace IW4MAdmin.Application
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider =>
{
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();

View File

@ -0,0 +1,21 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of the IMatchResult
/// used to hold matching results
/// </summary>
public class ParserMatchResult : IMatchResult
{
/// <summary>
/// array of matched pattern groups
/// </summary>
public string[] Values { get; set; }
/// <summary>
/// indicates if the match succeeded
/// </summary>
public bool Success { get; set; }
}
}

View File

@ -12,15 +12,11 @@ using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers
{
#if DEBUG
public class BaseRConParser : IRConParser
#else
class BaseRConParser : IRConParser
#endif
{
public BaseRConParser()
public BaseRConParser(IParserRegexFactory parserRegexFactory)
{
Configuration = new DynamicRConParserConfiguration()
Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
{
CommandPrefixes = new CommandPrefix()
{
@ -90,7 +86,6 @@ namespace IW4MAdmin.Application.RconParsers
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = removeTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue);
@ -134,7 +129,11 @@ namespace IW4MAdmin.Application.RconParsers
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
}
private List<EFClient> ClientsFromStatus(string[] Status)

View File

@ -1,4 +1,6 @@
namespace IW4MAdmin.Application.RconParsers
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
@ -6,5 +8,8 @@
/// </summary>
sealed internal class DynamicRConParser : BaseRConParser
{
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
{
}
}
}

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Interfaces;
using IW4MAdmin.Application.Factories;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System.Globalization;
@ -11,11 +12,18 @@ namespace IW4MAdmin.Application.RconParsers
sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration
{
public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; } = new ParserRegex();
public ParserRegex MapStatus { get; set; } = new ParserRegex();
public ParserRegex Dvar { get; set; } = new ParserRegex();
public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; }
public ParserRegex Dvar { get; set; }
public string ServerNotRunningResponse { get; set; }
public bool WaitForResponse { get; set; } = true;
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{
Status = parserRegexFactory.CreateParserRegex();
MapStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex();
}
}
}

View File

@ -1,4 +1,5 @@
using IW4MAdmin.Application;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
@ -42,7 +43,6 @@ namespace Tests
Manager.ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("test");
Manager.ConfigHandler.Set(config);
Manager.AdditionalRConParsers.Add(new TestRconParser());
Manager.Init().Wait();

View File

@ -10,6 +10,11 @@ namespace Tests
{
class TestRconParser : IW4MAdmin.Application.RconParsers.BaseRConParser
{
public TestRconParser(IParserRegexFactory f) : base(f)
{
}
public int FakeClientCount { get; set; }
public List<EFClient> FakeClients { get; set; } = new List<EFClient>();

View File

@ -2,7 +2,7 @@
@{
Layout = null;
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set;
double getDeviation(double deviations) => Math.Pow(Math.E, 5.0813 + (deviations * 0.8694));
double getDeviation(double deviations) => Math.Pow(Math.E, 5.259 + (deviations * 0.812));
string rankIcon(double elo)
{
if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25))

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Interfaces
{
@ -41,10 +40,21 @@ namespace SharedLibraryCore.Interfaces
AdditionalGroup = 200
}
public IParserPatternMatcher PatternMatcher { get; private set; }
private string pattern;
/// <summary>
/// stores the regular expression groups that will be mapped to group types
/// </summary>
public string Pattern { get; set; }
public string Pattern
{
get => pattern;
set
{
pattern = value;
PatternMatcher.Compile(value);
}
}
/// <summary>
/// stores the mapping from group type to group index in the regular expression
@ -90,9 +100,10 @@ namespace SharedLibraryCore.Interfaces
}
}
public ParserRegex()
public ParserRegex(IParserPatternMatcher pattern)
{
GroupMapping = new Dictionary<GroupType, int>();
PatternMatcher = pattern;
}
}
}

View File

@ -39,6 +39,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
ParserRegex Action { get; set; }
/// <summary>
/// stores the regex information for the time prefix in game log
/// </summary>
ParserRegex Time { get; set; }
/// <summary>
/// indicates the format expected for parsed guids
/// </summary>

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SharedLibraryCore.Interfaces
@ -17,11 +15,13 @@ namespace SharedLibraryCore.Interfaces
/// <param name="fileSizeDiff"></param>
/// <param name="startPosition"></param>
/// <returns></returns>
Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition);
Task<IEnumerable<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition);
/// <summary>
/// how long the log file is
/// </summary>
long Length { get; }
/// <summary>
/// how often to poll the log file
/// </summary>

View File

@ -0,0 +1,18 @@
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// represents a pattern match result
/// </summary>
public interface IMatchResult
{
/// <summary>
/// array of matched pattern groups
/// </summary>
string[] Values { get; set; }
/// <summary>
/// indicates if the match succeeded
/// </summary>
bool Success { get; set; }
}
}

View File

@ -0,0 +1,21 @@
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// defines the capabilities of a parser pattern
/// </summary>
public interface IParserPatternMatcher
{
/// <summary>
/// converts input string into pattern groups
/// </summary>
/// <param name="input">input string</param>
/// <returns>group matches</returns>
IMatchResult Match(string input);
/// <summary>
/// compiles the pattern to be used for matching
/// </summary>
/// <param name="pattern"></param>
void Compile(string pattern);
}
}

View File

@ -0,0 +1,14 @@
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// defines the capabilities of the parser regex factory
/// </summary>
public interface IParserRegexFactory
{
/// <summary>
/// creates a new ParserRegex instance
/// </summary>
/// <returns>ParserRegex instance</returns>
ParserRegex CreateParserRegex();
}
}

View File

@ -46,30 +46,30 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.1" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Npgsql" Version="4.1.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.1.2" />
<PackageReference Include="Npgsql" Version="4.1.3.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.3" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@ -20,6 +20,9 @@
</ItemGroup>
<ItemGroup>
<None Update="Files\GameEvents.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Files\T6Game.log">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View File

@ -0,0 +1,58 @@
using ApplicationTests.Fixtures;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using NUnit.Framework;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
namespace ApplicationTests
{
[TestFixture]
public class BaseEventParserTests
{
private EventLogTest eventLogData;
private IServiceProvider serviceProvider;
[SetUp]
public void Setup()
{
eventLogData = JsonConvert.DeserializeObject<EventLogTest>(System.IO.File.ReadAllText("Files/GameEvents.json"));
serviceProvider = new ServiceCollection()
.AddSingleton<BaseEventParser>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.BuildServiceProvider();
}
[Test]
public void TestParsesAllEventData()
{
var eventParser = serviceProvider.GetService<BaseEventParser>();
void AssertMatch(GameEvent src, LogEvent expected)
{
Assert.AreEqual(expected.ExpectedEventType, src.Type);
Assert.AreEqual(expected.ExpectedData, src.Data);
Assert.AreEqual(expected.ExpectedMessage, src.Message);
Assert.AreEqual(expected.ExpectedTime, src.GameTime);
//Assert.AreEqual(expected.ExpectedOriginClientName, src.Origin?.Name);
Assert.AreEqual(expected.ExpectedOriginClientNumber, src.Origin?.ClientNumber);
Assert.AreEqual(expected.ExpectedOriginNetworkId, src.Origin?.NetworkId.ToString("X"));
//Assert.AreEqual(expected.ExpectedTargetClientName, src.Target?.Name);
Assert.AreEqual(expected.ExpectedTargetClientNumber, src.Target?.ClientNumber);
Assert.AreEqual(expected.ExpectedTargetNetworkId, src.Target?.NetworkId.ToString("X"));
}
foreach (var e in eventLogData.Events)
{
var parsedEvent = eventParser.GenerateGameEvent(e.EventLine);
AssertMatch(parsedEvent, e);
}
}
}
}

View File

@ -0,0 +1,47 @@
using FakeItEasy;
using IW4MAdmin.Application.RconParsers;
using NUnit.Framework;
using SharedLibraryCore.Interfaces;
namespace ApplicationTests
{
[TestFixture]
public class BaseRConParserTests
{
[Test]
public void SetDvarAsync_FormatStringType()
{
var parser = new BaseRConParser(A.Fake<IParserRegexFactory>());
var connection = A.Fake<IRConConnection>();
parser.SetDvarAsync(connection, "test", "test").Wait();
A.CallTo(() => connection.SendQueryAsync(SharedLibraryCore.RCon.StaticHelpers.QueryType.SET_DVAR, "test \"test\""))
.MustHaveHappened();
}
[Test]
public void SetDvarAsync_FormatEmptyStringTypeIncludesQuotes()
{
var parser = new BaseRConParser(A.Fake<IParserRegexFactory>());
var connection = A.Fake<IRConConnection>();
parser.SetDvarAsync(connection, "test", "").Wait();
A.CallTo(() => connection.SendQueryAsync(SharedLibraryCore.RCon.StaticHelpers.QueryType.SET_DVAR, "test \"\""))
.MustHaveHappened();
}
[Test]
public void SetDvarAsync_FormatsNonString()
{
var parser = new BaseRConParser(A.Fake<IParserRegexFactory>());
var connection = A.Fake<IRConConnection>();
parser.SetDvarAsync(connection, "test", 123).Wait();
A.CallTo(() => connection.SendQueryAsync(SharedLibraryCore.RCon.StaticHelpers.QueryType.SET_DVAR, "test 123"))
.MustHaveHappened();
}
}
}

View File

@ -0,0 +1,26 @@
using static SharedLibraryCore.GameEvent;
using static SharedLibraryCore.Server;
namespace ApplicationTests.Fixtures
{
class LogEvent
{
public Game Game { get; set; }
public string EventLine { get; set; }
public EventType ExpectedEventType { get; set; }
public string ExpectedData { get; set; }
public string ExpectedMessage { get; set; }
public string ExpectedOriginNetworkId { get; set; }
public int? ExpectedOriginClientNumber { get; set; }
public string ExpectedOriginClientName { get; set; }
public string ExpectedTargetNetworkId { get; set; }
public int? ExpectedTargetClientNumber { get; set; }
public string ExpectedTargetClientName { get; set; }
public int? ExpectedTime { get; set; }
}
class EventLogTest
{
public LogEvent[] Events { get; set; }
}
}

View File

@ -0,0 +1,42 @@
using FakeItEasy;
using IW4MAdmin.Application.IO;
using NUnit.Framework;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
namespace ApplicationTests
{
[TestFixture]
public class IOTests
{
[Test]
public async Task GameLogEventDetection_WorksAfterFileSizeReset()
{
var reader = A.Fake<IGameLogReader>();
var detect = new GameLogEventDetection(null, "", A.Fake<Uri>(), reader);
A.CallTo(() => reader.Length)
.Returns(100)
.Once()
.Then
.Returns(200)
.Once()
.Then
.Returns(10)
.Once()
.Then
.Returns(100);
for (int i = 0; i < 4; i++)
{
await detect.UpdateLogEvents();
}
A.CallTo(() => reader.ReadEventsFromLog(A<Server>.Ignored, A<long>.Ignored, A<long>.Ignored))
.MustHaveHappenedTwiceExactly();
}
}
}

View File

@ -35,7 +35,7 @@ namespace ApplicationTests
new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 },
A.Fake<ITranslationLookup>(), A.Fake<IRConConnectionFactory>());
var parser = new BaseEventParser();
var parser = new BaseEventParser(A.Fake<IParserRegexFactory>());
parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer;
var log = System.IO.File.ReadAllLines("Files\\T6MapRotation.log");
@ -61,7 +61,7 @@ namespace ApplicationTests
new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 },
A.Fake<ITranslationLookup>(), A.Fake<IRConConnectionFactory>());
var parser = new BaseEventParser();
var parser = new BaseEventParser(A.Fake<IParserRegexFactory>());
parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer;
var log = System.IO.File.ReadAllLines("Files\\T6Game.log");

View File

@ -56,7 +56,7 @@ namespace ApplicationTests
A.Fake<ITranslationLookup>(),
A.Fake<IRConConnectionFactory>());
var parser = new BaseEventParser();
var parser = new BaseEventParser(A.Fake<IParserRegexFactory>());
parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer;
var log = System.IO.File.ReadAllLines("Files\\T6GameStats.log");

View File

@ -70,7 +70,7 @@
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.3" />
</ItemGroup>
<ItemGroup>