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

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

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

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

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

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

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

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

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

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

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.IO
_parser = parser; _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 // allocate the bytes for the new log lines
List<string> logLines = new List<string>(); List<string> logLines = new List<string>();

@ -16,27 +16,27 @@ namespace IW4MAdmin.Application.IO
/// </summary> /// </summary>
class GameLogReaderHttp : IGameLogReader class GameLogReaderHttp : IGameLogReader
{ {
readonly IEventParser Parser; private readonly IEventParser _eventParser;
readonly IGameLogServer Api; private readonly IGameLogServer _logServerApi;
readonly string logPath; readonly string logPath;
private string lastKey = "next"; private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser) public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
{ {
this.logPath = logPath.ToBase64UrlSafeString(); ; this.logPath = logPath.ToBase64UrlSafeString();
Parser = parser; _eventParser = parser;
Api = RestClient.For<IGameLogServer>(gameLogServerUri); _logServerApi = RestClient.For<IGameLogServer>(gameLogServerUri);
} }
public long Length => -1; public long Length => -1;
public int UpdateInterval => 500; 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>(); var events = new List<GameEvent>();
string b64Path = logPath; string b64Path = logPath;
var response = await Api.Log(b64Path, lastKey); var response = await _logServerApi.Log(b64Path, lastKey);
lastKey = response.NextKey; lastKey = response.NextKey;
if (!response.Success && string.IsNullOrEmpty(lastKey)) if (!response.Success && string.IsNullOrEmpty(lastKey))
@ -48,17 +48,17 @@ namespace IW4MAdmin.Application.IO
else if (!string.IsNullOrWhiteSpace(response.Data)) else if (!string.IsNullOrWhiteSpace(response.Data))
{ {
// parse each line // parse each line
foreach (string eventLine in response.Data var lines = response.Data
.Split(Environment.NewLine) .Split(Environment.NewLine)
.Where(_line => _line.Length > 0)) .Where(_line => _line.Length > 0);
foreach (string eventLine in lines)
{ {
try 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); events.Add(gameEvent);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {gameEvent.Id} from http");
#endif
} }
catch (Exception e) catch (Exception e)

@ -25,7 +25,7 @@ namespace IW4MAdmin
public class IW4MServer : Server public class IW4MServer : Server
{ {
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex; private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEventDetection LogEvent; public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0; private int lastGameTime = 0;
@ -891,8 +891,8 @@ namespace IW4MAdmin
EventParser = Manager.AdditionalEventParsers EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion); .FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
RconParser = RconParser ?? new BaseRConParser(); RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? new BaseEventParser(); EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RemoteConnection.SetConfiguration(RconParser.Configuration); RemoteConnection.SetConfiguration(RconParser.Configuration);
@ -949,6 +949,14 @@ namespace IW4MAdmin
try try
{ {
var website = await this.GetDvarAsync<string>("_website"); 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; Website = website.Value;
} }

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

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

@ -12,15 +12,11 @@ using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RconParsers
{ {
#if DEBUG
public class BaseRConParser : IRConParser 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() CommandPrefixes = new CommandPrefix()
{ {
@ -90,7 +86,6 @@ namespace IW4MAdmin.Application.RconParsers
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = removeTrailingColorCode(value); value = removeTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue); defaultValue = removeTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue); latchedValue = removeTrailingColorCode(latchedValue);
@ -134,7 +129,11 @@ namespace IW4MAdmin.Application.RconParsers
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) 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) private List<EFClient> ClientsFromStatus(string[] Status)

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

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

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

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

@ -2,7 +2,7 @@
@{ @{
Layout = null; Layout = null;
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set; 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) string rankIcon(double elo)
{ {
if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25)) if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -70,7 +70,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'"> <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>
<ItemGroup> <ItemGroup>