From 59d69bd22ba60c8bac728b35bd818acdd86b700b Mon Sep 17 00:00:00 2001 From: RaidMax Date: Mon, 28 Feb 2022 20:44:30 -0600 Subject: [PATCH] add cancellation token for rcon connection to allow more granular control --- Application/Commands/MapAndGameTypeCommand.cs | 2 +- Application/IW4MServer.cs | 43 +++--- Application/RConParsers/BaseRConParser.cs | 45 +++--- Integrations/Cod/CodRConConnection.cs | 142 ++++++++++++------ Integrations/Source/SourceRConConnection.cs | 12 +- .../AutomessageFeed/AutomessageFeed.csproj | 2 +- Plugins/LiveRadar/LiveRadar.csproj | 2 +- Plugins/Login/Login.csproj | 2 +- .../ProfanityDeterment.csproj | 2 +- Plugins/Stats/Stats.csproj | 2 +- Plugins/Welcome/Welcome.csproj | 2 +- SharedLibraryCore/Commands/NativeCommands.cs | 2 +- .../Interfaces/IRConConnection.cs | 7 +- SharedLibraryCore/Interfaces/IRConParser.cs | 13 +- SharedLibraryCore/Server.cs | 16 +- SharedLibraryCore/SharedLibraryCore.csproj | 4 +- SharedLibraryCore/Utilities.cs | 21 +-- 17 files changed, 187 insertions(+), 132 deletions(-) diff --git a/Application/Commands/MapAndGameTypeCommand.cs b/Application/Commands/MapAndGameTypeCommand.cs index b969d3ec1..87c1ffbbe 100644 --- a/Application/Commands/MapAndGameTypeCommand.cs +++ b/Application/Commands/MapAndGameTypeCommand.cs @@ -92,7 +92,7 @@ namespace IW4MAdmin.Application.Commands _logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype); - await gameEvent.Owner.SetDvarAsync("g_gametype", gametype); + await gameEvent.Owner.SetDvarAsync("g_gametype", gametype, gameEvent.Owner.Manager.CancellationToken); gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map)); await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds); diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 682115a60..ad67d616c 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -321,7 +321,7 @@ namespace IW4MAdmin if (!string.IsNullOrEmpty(CustomSayName)) { - await this.SetDvarAsync("sv_sayname", CustomSayName); + await this.SetDvarAsync("sv_sayname", CustomSayName, Manager.CancellationToken); } Throttled = false; @@ -783,7 +783,7 @@ namespace IW4MAdmin async Task[]> PollPlayersAsync() { var currentClients = GetClientsAsList(); - var statusResponse = (await this.GetStatusAsync()); + var statusResponse = await this.GetStatusAsync(Manager.CancellationToken); var polledClients = statusResponse.Clients.AsEnumerable(); if (Manager.GetApplicationSettings().Configuration().IgnoreBots) @@ -1109,7 +1109,7 @@ namespace IW4MAdmin RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine); RemoteConnection.SetConfiguration(RconParser); - var version = await this.GetMappedDvarValueOrDefaultAsync("version"); + var version = await this.GetMappedDvarValueOrDefaultAsync("version", token: Manager.CancellationToken); Version = version.Value; GameName = Utilities.GetGame(version.Value ?? RconParser.Version); @@ -1126,7 +1126,7 @@ namespace IW4MAdmin Version = RconParser.Version; } - var svRunning = await this.GetMappedDvarValueOrDefaultAsync("sv_running"); + var svRunning = await this.GetMappedDvarValueOrDefaultAsync("sv_running", token: Manager.CancellationToken); if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") { @@ -1135,27 +1135,28 @@ namespace IW4MAdmin var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null; - string hostname = (await this.GetMappedDvarValueOrDefaultAsync("sv_hostname", "hostname", infoResponse)).Value; - string mapname = (await this.GetMappedDvarValueOrDefaultAsync("mapname", infoResponse: infoResponse)).Value; - int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync("sv_maxclients", infoResponse: infoResponse)).Value; - string gametype = (await this.GetMappedDvarValueOrDefaultAsync("g_gametype", "gametype", infoResponse)).Value; - var basepath = await this.GetMappedDvarValueOrDefaultAsync("fs_basepath"); - var basegame = await this.GetMappedDvarValueOrDefaultAsync("fs_basegame"); - var homepath = await this.GetMappedDvarValueOrDefaultAsync("fs_homepath"); - var game = (await this.GetMappedDvarValueOrDefaultAsync("fs_game", infoResponse: infoResponse)); - var logfile = await this.GetMappedDvarValueOrDefaultAsync("g_log"); - var logsync = await this.GetMappedDvarValueOrDefaultAsync("g_logsync"); - var ip = await this.GetMappedDvarValueOrDefaultAsync("net_ip"); - var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: ""); + var hostname = (await this.GetMappedDvarValueOrDefaultAsync("sv_hostname", "hostname", infoResponse, token: Manager.CancellationToken)).Value; + var mapname = (await this.GetMappedDvarValueOrDefaultAsync("mapname", infoResponse: infoResponse, token: Manager.CancellationToken)).Value; + var maxplayers = (await this.GetMappedDvarValueOrDefaultAsync("sv_maxclients", infoResponse: infoResponse, token: Manager.CancellationToken)).Value; + var gametype = (await this.GetMappedDvarValueOrDefaultAsync("g_gametype", "gametype", infoResponse, token: Manager.CancellationToken)).Value; + var basepath = await this.GetMappedDvarValueOrDefaultAsync("fs_basepath", token: Manager.CancellationToken); + var basegame = await this.GetMappedDvarValueOrDefaultAsync("fs_basegame", token: Manager.CancellationToken); + var homepath = await this.GetMappedDvarValueOrDefaultAsync("fs_homepath", token: Manager.CancellationToken); + var game = await this.GetMappedDvarValueOrDefaultAsync("fs_game", infoResponse: infoResponse, token: Manager.CancellationToken); + var logfile = await this.GetMappedDvarValueOrDefaultAsync("g_log", token: Manager.CancellationToken); + var logsync = await this.GetMappedDvarValueOrDefaultAsync("g_logsync", token: Manager.CancellationToken); + var ip = await this.GetMappedDvarValueOrDefaultAsync("net_ip", token: Manager.CancellationToken); + var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken); if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName) { - await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName); + await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName, + Manager.CancellationToken); } try { - var website = await this.GetMappedDvarValueOrDefaultAsync("_website"); + var website = await this.GetMappedDvarValueOrDefaultAsync("_website", token: Manager.CancellationToken); // this occurs for games that don't give us anything back when // the dvar is not set @@ -1201,14 +1202,14 @@ namespace IW4MAdmin if (logsync.Value == 0) { - await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4 + await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4 needsRestart = true; } if (string.IsNullOrWhiteSpace(logfile.Value)) { logfile.Value = "games_mp.log"; - await this.SetDvarAsync("g_log", logfile.Value); + await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken); needsRestart = true; } @@ -1220,7 +1221,7 @@ namespace IW4MAdmin } // this DVAR isn't set until the a map is loaded - await this.SetDvarAsync("logfile", 2); + await this.SetDvarAsync("logfile", 2, Manager.CancellationToken); } CustomCallback = await ScriptLoaded(); diff --git a/Application/RConParsers/BaseRConParser.cs b/Application/RConParsers/BaseRConParser.cs index fcf128215..2740f0b8c 100644 --- a/Application/RConParsers/BaseRConParser.cs +++ b/Application/RConParsers/BaseRConParser.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Data.Models; using Microsoft.Extensions.Logging; @@ -77,19 +78,19 @@ namespace IW4MAdmin.Application.RConParsers public string RConEngine { get; set; } = "COD"; public bool IsOneLog { get; set; } - public async Task ExecuteCommandAsync(IRConConnection connection, string command) + public async Task ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default) { - var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); + var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray(); } - public async Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default) + public async Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default) { string[] lineSplit; try { - lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); + lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName, token); } catch { @@ -98,10 +99,10 @@ namespace IW4MAdmin.Application.RConParsers throw; } - lineSplit = new string[0]; + lineSplit = Array.Empty(); } - string response = string.Join('\n', lineSplit).TrimEnd('\0'); + var response = string.Join('\n', lineSplit).TrimEnd('\0'); var match = Regex.Match(response, Configuration.Dvar.Pattern); if (response.Contains("Unknown command") || @@ -109,7 +110,7 @@ namespace IW4MAdmin.Application.RConParsers { if (fallbackValue != null) { - return new Dvar() + return new Dvar { Name = dvarName, Value = fallbackValue @@ -119,17 +120,17 @@ namespace IW4MAdmin.Application.RConParsers throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); } - string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value; - string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value; - string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value; + var value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value; + var defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value; + var latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value; - string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); + string RemoveTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); - value = removeTrailingColorCode(value); - defaultValue = removeTrailingColorCode(defaultValue); - latchedValue = removeTrailingColorCode(latchedValue); + value = RemoveTrailingColorCode(value); + defaultValue = RemoveTrailingColorCode(defaultValue); + latchedValue = RemoveTrailingColorCode(latchedValue); - return new Dvar() + return new Dvar { Name = dvarName, Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), @@ -139,10 +140,12 @@ namespace IW4MAdmin.Application.RConParsers }; } - public virtual async Task GetStatusAsync(IRConConnection connection) + public virtual async Task GetStatusAsync(IRConConnection connection, CancellationToken token = default) { - var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); - _logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response)); + var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token); + + _logger.LogDebug("Status Response {Response}", string.Join(Environment.NewLine, response)); + return new StatusResponse { Clients = ClientsFromStatus(response).ToArray(), @@ -183,13 +186,13 @@ namespace IW4MAdmin.Application.RConParsers return (T)Convert.ChangeType(value, typeof(T)); } - public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) + public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default) { - string dvarString = (dvarValue is string str) + var dvarString = (dvarValue is string str) ? $"{dvarName} \"{str}\"" : $"{dvarName} {dvarValue}"; - return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0; + return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0; } private List ClientsFromStatus(string[] Status) diff --git a/Integrations/Cod/CodRConConnection.cs b/Integrations/Cod/CodRConConnection.cs index 4ac4d5b90..1e299a7bb 100644 --- a/Integrations/Cod/CodRConConnection.cs +++ b/Integrations/Cod/CodRConConnection.cs @@ -23,7 +23,7 @@ namespace Integrations.Cod /// public class CodRConConnection : IRConConnection { - static readonly ConcurrentDictionary ActiveQueries = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary ActiveQueries = new(); public IPEndPoint Endpoint { get; } public string RConPassword { get; } @@ -48,7 +48,29 @@ namespace Integrations.Cod config = parser.Configuration; } - public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") + public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", + CancellationToken token = default) + { + try + { + return await SendQueryAsyncInternal(type, parameters, token); + } + catch (OperationCanceledException) + { + _log.LogWarning("Timed out waiting for RCon response"); + throw new RConException("Did not received RCon response in allocated time frame"); + } + + finally + { + if (ActiveQueries[Endpoint].OnComplete.CurrentCount == 0) + { + ActiveQueries[Endpoint].OnComplete.Release(1); + ActiveQueries[Endpoint].ConnectionAttempts = 0; + } + } + } + private async Task SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default) { if (!ActiveQueries.ContainsKey(this.Endpoint)) { @@ -57,36 +79,36 @@ namespace Integrations.Cod var connectionState = ActiveQueries[this.Endpoint]; - _log.LogDebug("Waiting for semaphore to be released [{endpoint}]", Endpoint); + _log.LogDebug("Waiting for semaphore to be released [{Endpoint}]", Endpoint); // enter the semaphore so only one query is sent at a time per server. - await connectionState.OnComplete.WaitAsync(); + await connectionState.OnComplete.WaitAsync(token); var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds; if (timeSinceLastQuery < config.FloodProtectInterval) { - await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery); + await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery, token); } connectionState.LastQuery = DateTime.Now; - _log.LogDebug("Semaphore has been released [{endpoint}]", Endpoint); - _log.LogDebug("Query {@queryInfo}", new { endpoint=Endpoint.ToString(), type, parameters }); + _log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint); + _log.LogDebug("Query {@QueryInfo}", new { endpoint=Endpoint.ToString(), type, parameters }); byte[] payload = null; - bool waitForResponse = config.WaitForResponse; + var waitForResponse = config.WaitForResponse; - string convertEncoding(string text) + string ConvertEncoding(string text) { - byte[] convertedBytes = Utilities.EncodingType.GetBytes(text); + var convertedBytes = Utilities.EncodingType.GetBytes(text); return _gameEncoding.GetString(convertedBytes); } try { - string convertedRConPassword = convertEncoding(RConPassword); - string convertedParameters = convertEncoding(parameters); + var convertedRConPassword = ConvertEncoding(RConPassword); + var convertedParameters = ConvertEncoding(parameters); switch (type) { @@ -137,7 +159,7 @@ namespace Integrations.Cod using (LogContext.PushProperty("Server", Endpoint.ToString())) { _log.LogInformation( - "Retrying RCon message ({connectionAttempts}/{allowedConnectionFailures} attempts) with parameters {payload}", + "Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {payload}", connectionState.ConnectionAttempts, _retryAttempts, parameters); } @@ -151,20 +173,27 @@ namespace Integrations.Cod { connectionState.SendEventArgs.UserToken = socket; connectionState.ConnectionAttempts++; - await connectionState.OnSentData.WaitAsync(); - await connectionState.OnReceivedData.WaitAsync(); + await connectionState.OnSentData.WaitAsync(token); + await connectionState.OnReceivedData.WaitAsync(token); connectionState.BytesReadPerSegment.Clear(); - bool exceptionCaught = false; + var exceptionCaught = false; - _log.LogDebug("Sending {payloadLength} bytes to [{endpoint}] ({connectionAttempts}/{allowedConnectionFailures})", + _log.LogDebug("Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})", payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts); try { - - response = await SendPayloadAsync(payload, waitForResponse, parser.OverrideTimeoutForCommand(parameters)); - - if ((response.Length == 0 || response[0].Length == 0) && waitForResponse) + try + { + response = await SendPayloadAsync(payload, waitForResponse, + parser.OverrideTimeoutForCommand(parameters), token); + } + catch (OperationCanceledException) + { + // ignored + } + + if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse) { throw new RConException("Expected response but got 0 bytes back"); } @@ -178,14 +207,14 @@ namespace Integrations.Cod if (connectionState.ConnectionAttempts < _retryAttempts) { exceptionCaught = true; - await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts)); + await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token); goto retrySend; } using (LogContext.PushProperty("Server", Endpoint.ToString())) { _log.LogWarning( - "Made {connectionAttempts} attempts to send RCon data to server, but received no response", + "Made {ConnectionAttempts} attempts to send RCon data to server, but received no response", connectionState.ConnectionAttempts); } connectionState.ConnectionAttempts = 0; @@ -214,14 +243,15 @@ namespace Integrations.Cod if (response.Length == 0) { - _log.LogDebug("Received empty response for RCon request {@query}", new { endpoint=Endpoint.ToString(), type, parameters }); - return new string[0]; + _log.LogDebug("Received empty response for RCon request {@Query}", + new { endpoint = Endpoint.ToString(), type, parameters }); + return Array.Empty(); } - string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? + var responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? ReassembleSegmentedStatus(response) : RecombineMessages(response); - // note: not all games respond if the pasword is wrong or not set + // note: not all games respond if the password is wrong or not set if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) { throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]); @@ -237,21 +267,21 @@ namespace Integrations.Cod throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); } - string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value; - string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch); + var responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value; + var headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch); if (headerSplit.Length != 2) { using (LogContext.PushProperty("Server", Endpoint.ToString())) { - _log.LogWarning("Invalid response header from server. Expected {expected}, but got {response}", + _log.LogWarning("Invalid response header from server. Expected {Expected}, but got {Response}", config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault()); } throw new RConException("Unexpected response header from server"); } - string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + var splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); return splitResponse; } @@ -312,7 +342,7 @@ namespace Integrations.Cod } } - private async Task SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout) + private async Task SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout, CancellationToken token = default) { var connectionState = ActiveQueries[this.Endpoint]; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; @@ -332,18 +362,27 @@ namespace Integrations.Cod connectionState.SendEventArgs.SetBuffer(payload); // send the data to the server - bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs); + var sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs); if (sendDataPending) { // the send has not been completed asynchronously // this really shouldn't ever happen because it's UDP + var complete = false; + try + { + complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(1), token); + } + catch (OperationCanceledException) + { + // ignored + } - if(!await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(1))) + if(!complete) { using(LogContext.PushProperty("Server", Endpoint.ToString())) { - _log.LogWarning("Socket timed out while sending RCon data on attempt {attempt}", + _log.LogWarning("Socket timed out while sending RCon data on attempt {Attempt}", connectionState.ConnectionAttempts); } rconSocket.Close(); @@ -359,17 +398,29 @@ namespace Integrations.Cod connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer); // get our response back - bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs); + var receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs); if (receiveDataPending) { - _log.LogDebug("Waiting to asynchronously receive data on attempt #{connectionAttempts}", connectionState.ConnectionAttempts); - if (!await connectionState.OnReceivedData.WaitAsync( - new[] - { - StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), - overrideTimeout - }.Max())) + _log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}", connectionState.ConnectionAttempts); + + var completed = false; + + try + { + completed = await connectionState.OnReceivedData.WaitAsync( + new[] + { + StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), + overrideTimeout + }.Max(), token); + } + catch (OperationCanceledException) + { + // ignored + } + + if (!completed) { if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections { @@ -388,16 +439,15 @@ namespace Integrations.Cod } rconSocket.Close(); - return GetResponseData(connectionState); } private byte[][] GetResponseData(ConnectionState connectionState) { var responseList = new List(); - int totalBytesRead = 0; + var totalBytesRead = 0; - foreach (int bytesRead in connectionState.BytesReadPerSegment) + foreach (var bytesRead in connectionState.BytesReadPerSegment) { responseList.Add(connectionState.ReceiveBuffer .Skip(totalBytesRead) diff --git a/Integrations/Source/SourceRConConnection.cs b/Integrations/Source/SourceRConConnection.cs index 7578f1b48..623d80e81 100644 --- a/Integrations/Source/SourceRConConnection.cs +++ b/Integrations/Source/SourceRConConnection.cs @@ -48,12 +48,12 @@ namespace Integrations.Source _activeQuery.Dispose(); } - public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") + public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default) { try { - await _activeQuery.WaitAsync(); - await WaitForAvailable(); + await _activeQuery.WaitAsync(token); + await WaitForAvailable(token); if (_needNewSocket) { @@ -66,7 +66,7 @@ namespace Integrations.Source // ignored } - await Task.Delay(ConnectionTimeout); + await Task.Delay(ConnectionTimeout, token); _rconClient = _rconClientFactory.CreateClient(_ipEndPoint); _authenticated = false; _needNewSocket = false; @@ -147,12 +147,12 @@ namespace Integrations.Source } } - private async Task WaitForAvailable() + private async Task WaitForAvailable(CancellationToken token) { var diff = DateTime.Now - _lastQuery; if (diff < FloodDelay) { - await Task.Delay(FloodDelay - diff); + await Task.Delay(FloodDelay - diff, token); } } diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index 0b84a7f37..88bda769d 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index de9b0dbcc..874defcd7 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -23,7 +23,7 @@ - + diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index 40f6277bb..4ffa9ed54 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -19,7 +19,7 @@ - + diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index 46d401c75..d3a465baa 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index b37a46332..975218976 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -17,7 +17,7 @@ - + diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index ba4087a27..2fa4192a3 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -20,7 +20,7 @@ - + diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 482fc2790..efed1ffc4 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -1190,7 +1190,7 @@ namespace SharedLibraryCore.Commands public static async Task GetNextMap(Server s, ITranslationLookup lookup) { - var mapRotation = (await s.GetDvarAsync("sv_mapRotation")).Value?.ToLower() ?? ""; + var mapRotation = (await s.GetDvarAsync("sv_mapRotation", token: s.Manager.CancellationToken)).Value?.ToLower() ?? ""; var regexMatches = Regex.Matches(mapRotation, @"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase) .ToList(); diff --git a/SharedLibraryCore/Interfaces/IRConConnection.cs b/SharedLibraryCore/Interfaces/IRConConnection.cs index a7dfaa8b0..da56406dc 100644 --- a/SharedLibraryCore/Interfaces/IRConConnection.cs +++ b/SharedLibraryCore/Interfaces/IRConConnection.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using SharedLibraryCore.RCon; namespace SharedLibraryCore.Interfaces @@ -14,7 +15,7 @@ namespace SharedLibraryCore.Interfaces /// type of RCon query to perform /// optional parameter list /// - Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = ""); + Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default); /// /// sets the rcon parser @@ -22,4 +23,4 @@ namespace SharedLibraryCore.Interfaces /// parser void SetConfiguration(IRConParser config); } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index 2b630c60e..4b9e59f33 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using static SharedLibraryCore.Server; @@ -52,7 +53,7 @@ namespace SharedLibraryCore.Interfaces /// name of DVAR /// default value to return if dvar retrieval fails /// - Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default); + Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default); /// /// set value of DVAR by name @@ -61,7 +62,7 @@ namespace SharedLibraryCore.Interfaces /// name of DVAR to set /// value to set DVAR to /// - Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue); + Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default); /// /// executes a console command on the server @@ -69,8 +70,8 @@ namespace SharedLibraryCore.Interfaces /// RCon connection to use /// console command to execute /// - Task ExecuteCommandAsync(IRConConnection connection, string command); - + Task ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default); + /// /// get the list of connected clients from status response /// @@ -78,7 +79,7 @@ namespace SharedLibraryCore.Interfaces /// /// /// - Task GetStatusAsync(IRConConnection connection); + Task GetStatusAsync(IRConConnection connection, CancellationToken token = default); /// /// retrieves the value of given dvar key if it exists in the override dict @@ -103,4 +104,4 @@ namespace SharedLibraryCore.Interfaces /// TimeSpan OverrideTimeoutForCommand(string command); } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 1799fb76f..76729b97f 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -377,7 +377,7 @@ namespace SharedLibraryCore { try { - return (await this.GetDvarAsync("sv_customcallbacks", "0")).Value == "1"; + return (await this.GetDvarAsync("sv_customcallbacks", "0", Manager.CancellationToken)).Value == "1"; } catch (DvarException) @@ -391,11 +391,11 @@ namespace SharedLibraryCore public string[] ExecuteServerCommand(string command) { var tokenSource = new CancellationTokenSource(); - tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); + tokenSource.CancelAfter(TimeSpan.FromSeconds(1)); try { - return this.ExecuteCommandAsync(command).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); + return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult(); } catch { @@ -406,11 +406,10 @@ namespace SharedLibraryCore public string GetServerDvar(string dvarName) { var tokenSource = new CancellationTokenSource(); - tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); + tokenSource.CancelAfter(TimeSpan.FromSeconds(1)); try { - return this.GetDvarAsync(dvarName).WithWaitCancellation(tokenSource.Token).GetAwaiter() - .GetResult()?.Value; + return this.GetDvarAsync(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value; } catch { @@ -421,12 +420,11 @@ namespace SharedLibraryCore public bool SetServerDvar(string dvarName, string dvarValue) { var tokenSource = new CancellationTokenSource(); - tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); + tokenSource.CancelAfter(TimeSpan.FromSeconds(1)); try { - this.SetDvarAsync(dvarName, dvarValue).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); + this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult(); return true; - } catch { diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 3684c32cd..afdded57d 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -4,7 +4,7 @@ Library net6.0 RaidMax.IW4MAdmin.SharedLibraryCore - 2022.2.22.1 + 2022.2.28.1 RaidMax Forever None Debug;Release;Prerelease @@ -19,7 +19,7 @@ true MIT Shared Library for IW4MAdmin - 2022.2.22.1 + 2022.2.28.1 diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 02d5e8a11..dfd859064 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -723,14 +723,15 @@ namespace SharedLibraryCore .Replace('/', '_'); } - public static Task> GetDvarAsync(this Server server, string dvarName, T fallbackValue = default) + public static async Task> GetDvarAsync(this Server server, string dvarName, + T fallbackValue = default, CancellationToken token = default) { - return server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue); + return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token); } public static async Task> GetMappedDvarValueOrDefaultAsync(this Server server, string dvarName, string infoResponseName = null, IDictionary infoResponse = null, - T overrideDefault = default) + T overrideDefault = default, CancellationToken token = default) { // todo: unit test this var mappedKey = server.RconParser.GetOverrideDvarName(dvarName); @@ -749,22 +750,22 @@ namespace SharedLibraryCore }; } - return await server.GetDvarAsync(mappedKey, defaultValue); + return await server.GetDvarAsync(mappedKey, defaultValue, token: token); } - public static Task SetDvarAsync(this Server server, string dvarName, object dvarValue) + public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue, CancellationToken token = default) { - return server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue); + await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token); } - public static async Task ExecuteCommandAsync(this Server server, string commandName) + public static async Task ExecuteCommandAsync(this Server server, string commandName, CancellationToken token = default) { - return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName); + return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token); } - public static Task GetStatusAsync(this Server server) + public static Task GetStatusAsync(this Server server, CancellationToken token) { - return server.RconParser.GetStatusAsync(server.RemoteConnection); + return server.RconParser.GetStatusAsync(server.RemoteConnection, token); } ///