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

View File

@ -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<List<EFClient>[]> 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<string>("version");
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("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<string>("sv_running");
var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("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<string>("sv_hostname", "hostname", infoResponse)).Value;
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value;
var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath");
var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame");
var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath");
var game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse));
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip");
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "");
var hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse, token: Manager.CancellationToken)).Value;
var mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
var maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
var gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse, token: Manager.CancellationToken)).Value;
var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath", token: Manager.CancellationToken);
var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame", token: Manager.CancellationToken);
var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath", token: Manager.CancellationToken);
var game = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse, token: Manager.CancellationToken);
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log", token: Manager.CancellationToken);
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("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<string>("_website");
var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_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();

View File

@ -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<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();
}
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;
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>();
}
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<T>()
return new Dvar<T>
{
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<T>()
return new Dvar<T>
{
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<IStatusResponse> GetStatusAsync(IRConConnection connection)
public virtual async Task<IStatusResponse> 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<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} {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)

View File

@ -23,7 +23,7 @@ namespace Integrations.Cod
/// </summary>
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 string RConPassword { get; }
@ -48,7 +48,29 @@ namespace Integrations.Cod
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))
{
@ -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>();
}
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<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 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<byte[]>();
int totalBytesRead = 0;
var totalBytesRead = 0;
foreach (int bytesRead in connectionState.BytesReadPerSegment)
foreach (var bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)

View File

@ -48,12 +48,12 @@ namespace Integrations.Source
_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
{
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);
}
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target>
<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>
</Project>

View File

@ -1190,7 +1190,7 @@ namespace SharedLibraryCore.Commands
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,
@"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase)
.ToList();

View File

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

View File

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

View File

@ -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<string>(dvarName).WithWaitCancellation(tokenSource.Token).GetAwaiter()
.GetResult()?.Value;
return this.GetDvarAsync<string>(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
{

View File

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

View File

@ -723,14 +723,15 @@ namespace SharedLibraryCore
.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,
string infoResponseName = null, IDictionary<string, string> 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<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>