add cancellation token for rcon connection to allow more granular control

This commit is contained in:
RaidMax 2022-02-28 20:44:30 -06:00
parent e9c8ead829
commit 59d69bd22b
17 changed files with 187 additions and 132 deletions

View File

@ -92,7 +92,7 @@ namespace IW4MAdmin.Application.Commands
_logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype); _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)); gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map));
await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds); await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds);

View File

@ -321,7 +321,7 @@ namespace IW4MAdmin
if (!string.IsNullOrEmpty(CustomSayName)) if (!string.IsNullOrEmpty(CustomSayName))
{ {
await this.SetDvarAsync("sv_sayname", CustomSayName); await this.SetDvarAsync("sv_sayname", CustomSayName, Manager.CancellationToken);
} }
Throttled = false; Throttled = false;
@ -783,7 +783,7 @@ namespace IW4MAdmin
async Task<List<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync()
{ {
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync()); var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
var polledClients = statusResponse.Clients.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -1109,7 +1109,7 @@ namespace IW4MAdmin
RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine); RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine);
RemoteConnection.SetConfiguration(RconParser); RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version"); var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version", token: Manager.CancellationToken);
Version = version.Value; Version = version.Value;
GameName = Utilities.GetGame(version.Value ?? RconParser.Version); GameName = Utilities.GetGame(version.Value ?? RconParser.Version);
@ -1126,7 +1126,7 @@ namespace IW4MAdmin
Version = RconParser.Version; Version = RconParser.Version;
} }
var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running"); var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running", token: Manager.CancellationToken);
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") 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; var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
string hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse)).Value; var hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse, token: Manager.CancellationToken)).Value;
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value; var mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse)).Value; var maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value; var gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse, token: Manager.CancellationToken)).Value;
var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath"); var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath", token: Manager.CancellationToken);
var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame"); var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame", token: Manager.CancellationToken);
var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath"); var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath", token: Manager.CancellationToken);
var game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse)); var game = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse, token: Manager.CancellationToken);
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log"); var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log", token: Manager.CancellationToken);
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync"); var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip"); var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip", token: Manager.CancellationToken);
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: ""); var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken);
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName) 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 try
{ {
var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_website"); var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_website", token: Manager.CancellationToken);
// this occurs for games that don't give us anything back when // this occurs for games that don't give us anything back when
// the dvar is not set // the dvar is not set
@ -1201,14 +1202,14 @@ namespace IW4MAdmin
if (logsync.Value == 0) 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; needsRestart = true;
} }
if (string.IsNullOrWhiteSpace(logfile.Value)) if (string.IsNullOrWhiteSpace(logfile.Value))
{ {
logfile.Value = "games_mp.log"; logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value); await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true; needsRestart = true;
} }
@ -1220,7 +1221,7 @@ namespace IW4MAdmin
} }
// this DVAR isn't set until the a map is loaded // 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(); CustomCallback = await ScriptLoaded();

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -77,19 +78,19 @@ namespace IW4MAdmin.Application.RConParsers
public string RConEngine { get; set; } = "COD"; public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; } public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command) public async Task<string[]> 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(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default)
{ {
string[] lineSplit; string[] lineSplit;
try try
{ {
lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName, token);
} }
catch catch
{ {
@ -98,10 +99,10 @@ namespace IW4MAdmin.Application.RConParsers
throw; throw;
} }
lineSplit = new string[0]; lineSplit = Array.Empty<string>();
} }
string response = string.Join('\n', lineSplit).TrimEnd('\0'); var response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") || if (response.Contains("Unknown command") ||
@ -109,7 +110,7 @@ namespace IW4MAdmin.Application.RConParsers
{ {
if (fallbackValue != null) if (fallbackValue != null)
{ {
return new Dvar<T>() return new Dvar<T>
{ {
Name = dvarName, Name = dvarName,
Value = fallbackValue Value = fallbackValue
@ -119,17 +120,17 @@ namespace IW4MAdmin.Application.RConParsers
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
} }
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value; var value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value;
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value; var defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value;
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].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); value = RemoveTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue); defaultValue = RemoveTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue); latchedValue = RemoveTrailingColorCode(latchedValue);
return new Dvar<T>() return new Dvar<T>
{ {
Name = dvarName, Name = dvarName,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
@ -139,10 +140,12 @@ namespace IW4MAdmin.Application.RConParsers
}; };
} }
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection) public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
_logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response));
_logger.LogDebug("Status Response {Response}", string.Join(Environment.NewLine, response));
return new StatusResponse return new StatusResponse
{ {
Clients = ClientsFromStatus(response).ToArray(), Clients = ClientsFromStatus(response).ToArray(),
@ -183,13 +186,13 @@ namespace IW4MAdmin.Application.RConParsers
return (T)Convert.ChangeType(value, typeof(T)); return (T)Convert.ChangeType(value, typeof(T));
} }
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) public async Task<bool> 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} \"{str}\""
: $"{dvarName} {dvarValue}"; : $"{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<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)

View File

@ -23,7 +23,7 @@ namespace Integrations.Cod
/// </summary> /// </summary>
public class CodRConConnection : IRConConnection public class CodRConConnection : IRConConnection
{ {
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>(); private static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new();
public IPEndPoint Endpoint { get; } public IPEndPoint Endpoint { get; }
public string RConPassword { get; } public string RConPassword { get; }
@ -48,7 +48,29 @@ namespace Integrations.Cod
config = parser.Configuration; config = parser.Configuration;
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> 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<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
{ {
if (!ActiveQueries.ContainsKey(this.Endpoint)) if (!ActiveQueries.ContainsKey(this.Endpoint))
{ {
@ -57,36 +79,36 @@ namespace Integrations.Cod
var connectionState = ActiveQueries[this.Endpoint]; 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. // 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; var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
if (timeSinceLastQuery < config.FloodProtectInterval) if (timeSinceLastQuery < config.FloodProtectInterval)
{ {
await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery); await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery, token);
} }
connectionState.LastQuery = DateTime.Now; connectionState.LastQuery = DateTime.Now;
_log.LogDebug("Semaphore has been released [{endpoint}]", Endpoint); _log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint);
_log.LogDebug("Query {@queryInfo}", new { endpoint=Endpoint.ToString(), type, parameters }); _log.LogDebug("Query {@QueryInfo}", new { endpoint=Endpoint.ToString(), type, parameters });
byte[] payload = null; 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); return _gameEncoding.GetString(convertedBytes);
} }
try try
{ {
string convertedRConPassword = convertEncoding(RConPassword); var convertedRConPassword = ConvertEncoding(RConPassword);
string convertedParameters = convertEncoding(parameters); var convertedParameters = ConvertEncoding(parameters);
switch (type) switch (type)
{ {
@ -137,7 +159,7 @@ namespace Integrations.Cod
using (LogContext.PushProperty("Server", Endpoint.ToString())) using (LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogInformation( _log.LogInformation(
"Retrying RCon message ({connectionAttempts}/{allowedConnectionFailures} attempts) with parameters {payload}", "Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {payload}",
connectionState.ConnectionAttempts, connectionState.ConnectionAttempts,
_retryAttempts, parameters); _retryAttempts, parameters);
} }
@ -151,20 +173,27 @@ namespace Integrations.Cod
{ {
connectionState.SendEventArgs.UserToken = socket; connectionState.SendEventArgs.UserToken = socket;
connectionState.ConnectionAttempts++; connectionState.ConnectionAttempts++;
await connectionState.OnSentData.WaitAsync(); await connectionState.OnSentData.WaitAsync(token);
await connectionState.OnReceivedData.WaitAsync(); await connectionState.OnReceivedData.WaitAsync(token);
connectionState.BytesReadPerSegment.Clear(); 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); payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
try try
{ {
try
response = await SendPayloadAsync(payload, waitForResponse, parser.OverrideTimeoutForCommand(parameters)); {
response = await SendPayloadAsync(payload, waitForResponse,
if ((response.Length == 0 || response[0].Length == 0) && 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"); throw new RConException("Expected response but got 0 bytes back");
} }
@ -178,14 +207,14 @@ namespace Integrations.Cod
if (connectionState.ConnectionAttempts < _retryAttempts) if (connectionState.ConnectionAttempts < _retryAttempts)
{ {
exceptionCaught = true; exceptionCaught = true;
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts)); await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
goto retrySend; goto retrySend;
} }
using (LogContext.PushProperty("Server", Endpoint.ToString())) using (LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogWarning( _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);
} }
connectionState.ConnectionAttempts = 0; connectionState.ConnectionAttempts = 0;
@ -214,14 +243,15 @@ namespace Integrations.Cod
if (response.Length == 0) if (response.Length == 0)
{ {
_log.LogDebug("Received empty response for RCon request {@query}", new { endpoint=Endpoint.ToString(), type, parameters }); _log.LogDebug("Received empty response for RCon request {@Query}",
return new string[0]; new { endpoint = Endpoint.ToString(), type, parameters });
return Array.Empty<string>();
} }
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? var responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) : RecombineMessages(response); 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")) if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{ {
throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]); 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())); throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
} }
string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value; var responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value;
string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch); var headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
if (headerSplit.Length != 2) if (headerSplit.Length != 2)
{ {
using (LogContext.PushProperty("Server", Endpoint.ToString())) 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()); config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
} }
throw new RConException("Unexpected response header from server"); 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; return splitResponse;
} }
@ -312,7 +342,7 @@ namespace Integrations.Cod
} }
} }
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout) private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout, CancellationToken token = default)
{ {
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
@ -332,18 +362,27 @@ namespace Integrations.Cod
connectionState.SendEventArgs.SetBuffer(payload); connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server // send the data to the server
bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs); var sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending) if (sendDataPending)
{ {
// the send has not been completed asynchronously // the send has not been completed asynchronously
// this really shouldn't ever happen because it's UDP // 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())) 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); connectionState.ConnectionAttempts);
} }
rconSocket.Close(); rconSocket.Close();
@ -359,17 +398,29 @@ namespace Integrations.Cod
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer); connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back // get our response back
bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs); var receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending) if (receiveDataPending)
{ {
_log.LogDebug("Waiting to asynchronously receive data on attempt #{connectionAttempts}", connectionState.ConnectionAttempts); _log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}", connectionState.ConnectionAttempts);
if (!await connectionState.OnReceivedData.WaitAsync(
new[] var completed = false;
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), try
overrideTimeout {
}.Max())) 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 if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{ {
@ -388,16 +439,15 @@ namespace Integrations.Cod
} }
rconSocket.Close(); rconSocket.Close();
return GetResponseData(connectionState); return GetResponseData(connectionState);
} }
private byte[][] GetResponseData(ConnectionState connectionState) private byte[][] GetResponseData(ConnectionState connectionState)
{ {
var responseList = new List<byte[]>(); var responseList = new List<byte[]>();
int totalBytesRead = 0; var totalBytesRead = 0;
foreach (int bytesRead in connectionState.BytesReadPerSegment) foreach (var bytesRead in connectionState.BytesReadPerSegment)
{ {
responseList.Add(connectionState.ReceiveBuffer responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead) .Skip(totalBytesRead)

View File

@ -48,12 +48,12 @@ namespace Integrations.Source
_activeQuery.Dispose(); _activeQuery.Dispose();
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
{ {
try try
{ {
await _activeQuery.WaitAsync(); await _activeQuery.WaitAsync(token);
await WaitForAvailable(); await WaitForAvailable(token);
if (_needNewSocket) if (_needNewSocket)
{ {
@ -66,7 +66,7 @@ namespace Integrations.Source
// ignored // ignored
} }
await Task.Delay(ConnectionTimeout); await Task.Delay(ConnectionTimeout, token);
_rconClient = _rconClientFactory.CreateClient(_ipEndPoint); _rconClient = _rconClientFactory.CreateClient(_ipEndPoint);
_authenticated = false; _authenticated = false;
_needNewSocket = 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; var diff = DateTime.Now - _lastQuery;
if (diff < FloodDelay) if (diff < FloodDelay)
{ {
await Task.Delay(FloodDelay - diff); await Task.Delay(FloodDelay - diff, token);
} }
} }

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1190,7 +1190,7 @@ namespace SharedLibraryCore.Commands
public static async Task<string> GetNextMap(Server s, ITranslationLookup lookup) public static async Task<string> GetNextMap(Server s, ITranslationLookup lookup)
{ {
var mapRotation = (await s.GetDvarAsync<string>("sv_mapRotation")).Value?.ToLower() ?? ""; var mapRotation = (await s.GetDvarAsync<string>("sv_mapRotation", token: s.Manager.CancellationToken)).Value?.ToLower() ?? "";
var regexMatches = Regex.Matches(mapRotation, var regexMatches = Regex.Matches(mapRotation,
@"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase) @"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase)
.ToList(); .ToList();

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.RCon; using SharedLibraryCore.RCon;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
@ -14,7 +15,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="type">type of RCon query to perform</param> /// <param name="type">type of RCon query to perform</param>
/// <param name="parameters">optional parameter list</param> /// <param name="parameters">optional parameter list</param>
/// <returns></returns> /// <returns></returns>
Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = ""); Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default);
/// <summary> /// <summary>
/// sets the rcon parser /// sets the rcon parser
@ -22,4 +23,4 @@ namespace SharedLibraryCore.Interfaces
/// <param name="config">parser</param> /// <param name="config">parser</param>
void SetConfiguration(IRConParser config); void SetConfiguration(IRConParser config);
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -52,7 +53,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="dvarName">name of DVAR</param> /// <param name="dvarName">name of DVAR</param>
/// <param name="fallbackValue">default value to return if dvar retrieval fails</param> /// <param name="fallbackValue">default value to return if dvar retrieval fails</param>
/// <returns></returns> /// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default); Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
/// <summary> /// <summary>
/// set value of DVAR by name /// set value of DVAR by name
@ -61,7 +62,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="dvarName">name of DVAR to set</param> /// <param name="dvarName">name of DVAR to set</param>
/// <param name="dvarValue">value to set DVAR to</param> /// <param name="dvarValue">value to set DVAR to</param>
/// <returns></returns> /// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue); Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
/// <summary> /// <summary>
/// executes a console command on the server /// executes a console command on the server
@ -69,8 +70,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="connection">RCon connection to use</param> /// <param name="connection">RCon connection to use</param>
/// <param name="command">console command to execute</param> /// <param name="command">console command to execute</param>
/// <returns></returns> /// <returns></returns>
Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command); Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default);
/// <summary> /// <summary>
/// get the list of connected clients from status response /// get the list of connected clients from status response
/// </summary> /// </summary>
@ -78,7 +79,7 @@ namespace SharedLibraryCore.Interfaces
/// <returns> /// <returns>
/// <see cref="IStatusResponse" /> /// <see cref="IStatusResponse" />
/// </returns> /// </returns>
Task<IStatusResponse> GetStatusAsync(IRConConnection connection); Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default);
/// <summary> /// <summary>
/// retrieves the value of given dvar key if it exists in the override dict /// retrieves the value of given dvar key if it exists in the override dict
@ -103,4 +104,4 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
TimeSpan OverrideTimeoutForCommand(string command); TimeSpan OverrideTimeoutForCommand(string command);
} }
} }

View File

@ -377,7 +377,7 @@ namespace SharedLibraryCore
{ {
try try
{ {
return (await this.GetDvarAsync("sv_customcallbacks", "0")).Value == "1"; return (await this.GetDvarAsync("sv_customcallbacks", "0", Manager.CancellationToken)).Value == "1";
} }
catch (DvarException) catch (DvarException)
@ -391,11 +391,11 @@ namespace SharedLibraryCore
public string[] ExecuteServerCommand(string command) public string[] ExecuteServerCommand(string command)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(1));
try try
{ {
return this.ExecuteCommandAsync(command).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult();
} }
catch catch
{ {
@ -406,11 +406,10 @@ namespace SharedLibraryCore
public string GetServerDvar(string dvarName) public string GetServerDvar(string dvarName)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(1));
try try
{ {
return this.GetDvarAsync<string>(dvarName).WithWaitCancellation(tokenSource.Token).GetAwaiter() return this.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
.GetResult()?.Value;
} }
catch catch
{ {
@ -421,12 +420,11 @@ namespace SharedLibraryCore
public bool SetServerDvar(string dvarName, string dvarValue) public bool SetServerDvar(string dvarName, string dvarValue)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(1));
try try
{ {
this.SetDvarAsync(dvarName, dvarValue).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
return true; return true;
} }
catch catch
{ {

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.2.22.1</Version> <Version>2022.2.28.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.2.22.1</PackageVersion> <PackageVersion>2022.2.28.1</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">

View File

@ -723,14 +723,15 @@ namespace SharedLibraryCore
.Replace('/', '_'); .Replace('/', '_');
} }
public static Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName, T fallbackValue = default) public static async Task<Dvar<T>> GetDvarAsync<T>(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<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName, public static async Task<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName,
string infoResponseName = null, IDictionary<string, string> infoResponse = null, string infoResponseName = null, IDictionary<string, string> infoResponse = null,
T overrideDefault = default) T overrideDefault = default, CancellationToken token = default)
{ {
// todo: unit test this // todo: unit test this
var mappedKey = server.RconParser.GetOverrideDvarName(dvarName); 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<string[]> ExecuteCommandAsync(this Server server, string commandName) public static async Task<string[]> 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<IStatusResponse> GetStatusAsync(this Server server) public static Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
{ {
return server.RconParser.GetStatusAsync(server.RemoteConnection); return server.RconParser.GetStatusAsync(server.RemoteConnection, token);
} }
/// <summary> /// <summary>