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/fonts
/WebfrontCore/wwwroot/font /WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles /Plugins/Tests/TestSourceFiles
/Tests/ApplicationTests/Files/GameEvents.json

View File

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

View File

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

View File

@ -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,44 +80,48 @@ 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(';');
string eventType = lineSplit[0]; string eventType = lineSplit[0];
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);

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>
<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">

View File

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

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 }, 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");

View File

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

View File

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