tweak cod rcon connection and fix max health for hide integration command
This commit is contained in:
parent
59ca399045
commit
acf66da4ca
@ -129,6 +129,11 @@ PlayerConnectEvents()
|
|||||||
{
|
{
|
||||||
self endon( "disconnect" );
|
self endon( "disconnect" );
|
||||||
|
|
||||||
|
if ( IsDefined( self.isHidden ) && self.isHidden )
|
||||||
|
{
|
||||||
|
self HideImpl();
|
||||||
|
}
|
||||||
|
|
||||||
clientData = self.pers[level.clientDataKey];
|
clientData = self.pers[level.clientDataKey];
|
||||||
|
|
||||||
// this gives IW4MAdmin some time to register the player before making the request;
|
// this gives IW4MAdmin some time to register the player before making the request;
|
||||||
@ -591,18 +596,17 @@ HideImpl()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( IsDefined( self.isHidden ) && self.isHidden )
|
|
||||||
{
|
|
||||||
self IPrintLnBold( "You are already hidden" );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self SetClientDvar( "sv_cheats", 1 );
|
self SetClientDvar( "sv_cheats", 1 );
|
||||||
self SetClientDvar( "cg_thirdperson", 1 );
|
self SetClientDvar( "cg_thirdperson", 1 );
|
||||||
self SetClientDvar( "sv_cheats", 0 );
|
self SetClientDvar( "sv_cheats", 0 );
|
||||||
|
|
||||||
self.savedHealth = self.health;
|
if ( !IsDefined( self.savedHealth ) || self.health < 1000 )
|
||||||
self.health = 9999;
|
{
|
||||||
|
self.savedHealth = self.health;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.maxhealth = 99999;
|
||||||
|
self.health = 99999;
|
||||||
self.isHidden = true;
|
self.isHidden = true;
|
||||||
|
|
||||||
self Hide();
|
self Hide();
|
||||||
|
@ -44,14 +44,33 @@ namespace Integrations.Cod
|
|||||||
|
|
||||||
public void SetConfiguration(IRConParser parser)
|
public void SetConfiguration(IRConParser parser)
|
||||||
{
|
{
|
||||||
this._parser = parser;
|
_parser = parser;
|
||||||
_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)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return await SendQueryAsyncInternal(type, parameters, token);
|
try
|
||||||
|
{
|
||||||
|
return await SendQueryAsyncInternal(type, parameters, token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||||
|
{
|
||||||
|
_log.LogWarning(ex, "Could not complete RCon request");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (ActiveQueries[Endpoint].OnComplete.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
ActiveQueries[Endpoint].OnComplete.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
|
private async Task<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
|
||||||
@ -87,13 +106,6 @@ namespace Integrations.Cod
|
|||||||
{
|
{
|
||||||
throw new RConException("Timed out waiting for flood protect to expire");
|
throw new RConException("Timed out waiting for flood protect to expire");
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (connectionState.OnComplete.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
connectionState.OnComplete.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint);
|
_log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint);
|
||||||
@ -158,15 +170,7 @@ namespace Integrations.Cod
|
|||||||
_gameEncoding.EncodingName, parameters);
|
_gameEncoding.EncodingName, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RConException($"Invalid character encountered when converting encodings");
|
throw new RConException("Invalid character encountered when converting encodings");
|
||||||
}
|
|
||||||
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (connectionState.OnComplete.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
connectionState.OnComplete.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[][] response = null;
|
byte[][] response = null;
|
||||||
@ -182,14 +186,15 @@ namespace Integrations.Cod
|
|||||||
_retryAttempts, parameters);
|
_retryAttempts, parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
|
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
|
||||||
|
{
|
||||||
|
DontFragment = false,
|
||||||
|
Ttl = 100,
|
||||||
|
ExclusiveAddressUse = true,
|
||||||
|
})
|
||||||
{
|
{
|
||||||
DontFragment = false,
|
// wait for send to be ready
|
||||||
Ttl = 100,
|
|
||||||
ExclusiveAddressUse = true,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// wait for send to complete
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await connectionState.OnSentData.WaitAsync(token);
|
await connectionState.OnSentData.WaitAsync(token);
|
||||||
@ -198,15 +203,8 @@ namespace Integrations.Cod
|
|||||||
{
|
{
|
||||||
throw new RConException("Timed out waiting for access to RCon send socket");
|
throw new RConException("Timed out waiting for access to RCon send socket");
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (connectionState.OnComplete.CurrentCount == 0 )
|
|
||||||
{
|
|
||||||
connectionState.OnComplete.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for receive to complete
|
// wait for receive to be ready
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await connectionState.OnReceivedData.WaitAsync(token);
|
await connectionState.OnReceivedData.WaitAsync(token);
|
||||||
@ -217,25 +215,24 @@ namespace Integrations.Cod
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (connectionState.OnComplete.CurrentCount == 0 )
|
|
||||||
{
|
|
||||||
connectionState.OnComplete.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionState.OnSentData.CurrentCount == 0)
|
if (connectionState.OnSentData.CurrentCount == 0)
|
||||||
{
|
{
|
||||||
connectionState.OnSentData.Release();
|
connectionState.OnSentData.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionState.SendEventArgs.UserToken = socket;
|
connectionState.SendEventArgs.UserToken = new ConnectionUserToken
|
||||||
|
{
|
||||||
|
Socket = socket,
|
||||||
|
CancellationToken = token
|
||||||
|
};
|
||||||
|
|
||||||
connectionState.ConnectionAttempts++;
|
connectionState.ConnectionAttempts++;
|
||||||
|
|
||||||
connectionState.BytesReadPerSegment.Clear();
|
connectionState.BytesReadPerSegment.Clear();
|
||||||
var exceptionCaught = false;
|
|
||||||
|
|
||||||
_log.LogDebug("Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})",
|
_log.LogDebug(
|
||||||
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
|
"Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})",
|
||||||
|
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -262,17 +259,13 @@ namespace Integrations.Cod
|
|||||||
// we want to retry with a delay
|
// we want to retry with a delay
|
||||||
if (connectionState.ConnectionAttempts < _retryAttempts)
|
if (connectionState.ConnectionAttempts < _retryAttempts)
|
||||||
{
|
{
|
||||||
exceptionCaught = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(
|
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
|
||||||
token != CancellationToken.None
|
|
||||||
? StaticHelpers.SocketTimeout(100) // if using cancellation token we don't care about attempt count
|
|
||||||
: StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw new RConException("Timed out waiting on delay retry");
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
goto retrySend;
|
goto retrySend;
|
||||||
@ -284,29 +277,33 @@ namespace Integrations.Cod
|
|||||||
"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;
|
||||||
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
|
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// we don't want to release if we're going to retry the query
|
try
|
||||||
if (connectionState.OnComplete.CurrentCount == 0 && !exceptionCaught)
|
|
||||||
{
|
{
|
||||||
connectionState.OnComplete.Release();
|
if (connectionState.OnSentData.CurrentCount == 0)
|
||||||
}
|
{
|
||||||
|
connectionState.OnSentData.Release();
|
||||||
|
}
|
||||||
|
|
||||||
if (connectionState.OnSentData.CurrentCount == 0)
|
if (connectionState.OnReceivedData.CurrentCount == 0)
|
||||||
{
|
{
|
||||||
connectionState.OnSentData.Release();
|
connectionState.OnReceivedData.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
if (connectionState.OnReceivedData.CurrentCount == 0)
|
|
||||||
{
|
{
|
||||||
connectionState.OnReceivedData.Release();
|
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
|
||||||
|
// this thread is not notified because it's an event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// at this point we can run in parallel and the next request can start because we have our data
|
||||||
if (response.Length == 0)
|
if (response.Length == 0)
|
||||||
{
|
{
|
||||||
_log.LogDebug("Received empty response for RCon request {@Query}",
|
_log.LogDebug("Received empty response for RCon request {@Query}",
|
||||||
@ -357,7 +354,7 @@ namespace Integrations.Cod
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="segments">array of segmented byte arrays</param>
|
/// <param name="segments">array of segmented byte arrays</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private string ReassembleSegmentedStatus(byte[][] segments)
|
private string ReassembleSegmentedStatus(IEnumerable<byte[]> segments)
|
||||||
{
|
{
|
||||||
var splitStatusStrings = new List<string>();
|
var splitStatusStrings = new List<string>();
|
||||||
|
|
||||||
@ -391,28 +388,25 @@ namespace Integrations.Cod
|
|||||||
return _gameEncoding.GetString(payload[0]).TrimEnd('\n') + '\n';
|
return _gameEncoding.GetString(payload[0]).TrimEnd('\n') + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
var builder = new StringBuilder();
|
||||||
|
for (var i = 0; i < payload.Count; i++)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var message = _gameEncoding.GetString(payload[i]).TrimEnd('\n') + '\n';
|
||||||
for (int i = 0; i < payload.Count; i++)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
string message = _gameEncoding.GetString(payload[i]).TrimEnd('\n') + '\n';
|
message = message.Replace(_config.CommandPrefixes.RConResponse, "");
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
message = message.Replace(_config.CommandPrefixes.RConResponse, "");
|
|
||||||
}
|
|
||||||
builder.Append(message);
|
|
||||||
}
|
}
|
||||||
builder.Append('\n');
|
builder.Append(message);
|
||||||
return builder.ToString();
|
|
||||||
}
|
}
|
||||||
|
builder.Append('\n');
|
||||||
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var connectionState = ActiveQueries[Endpoint];
|
var connectionState = ActiveQueries[Endpoint];
|
||||||
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
|
var rconSocket = ((ConnectionUserToken)connectionState.SendEventArgs.UserToken)?.Socket;
|
||||||
|
|
||||||
if (rconSocket is null)
|
if (rconSocket is null)
|
||||||
{
|
{
|
||||||
@ -425,8 +419,8 @@ namespace Integrations.Cod
|
|||||||
// setup the event handlers only once because we're reusing the event args
|
// setup the event handlers only once because we're reusing the event args
|
||||||
connectionState.SendEventArgs.Completed += OnDataSent;
|
connectionState.SendEventArgs.Completed += OnDataSent;
|
||||||
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
|
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
|
||||||
connectionState.SendEventArgs.RemoteEndPoint = this.Endpoint;
|
connectionState.SendEventArgs.RemoteEndPoint = Endpoint;
|
||||||
connectionState.ReceiveEventArgs.RemoteEndPoint = this.Endpoint;
|
connectionState.ReceiveEventArgs.RemoteEndPoint = Endpoint;
|
||||||
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
|
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
|
||||||
connectionState.SendEventArgs.DisconnectReuseSocket = true;
|
connectionState.SendEventArgs.DisconnectReuseSocket = true;
|
||||||
}
|
}
|
||||||
@ -440,17 +434,9 @@ namespace Integrations.Cod
|
|||||||
{
|
{
|
||||||
// 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;
|
var complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(4), token);
|
||||||
try
|
|
||||||
{
|
|
||||||
complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(1), token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!complete)
|
if (!complete)
|
||||||
{
|
{
|
||||||
using(LogContext.PushProperty("Server", Endpoint.ToString()))
|
using(LogContext.PushProperty("Server", Endpoint.ToString()))
|
||||||
{
|
{
|
||||||
@ -459,12 +445,6 @@ namespace Integrations.Cod
|
|||||||
}
|
}
|
||||||
|
|
||||||
rconSocket.Close();
|
rconSocket.Close();
|
||||||
|
|
||||||
if (connectionState.OnSentData.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
connectionState.OnSentData.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NetworkException("Timed out sending RCon data", rconSocket);
|
throw new NetworkException("Timed out sending RCon data", rconSocket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -512,11 +492,6 @@ namespace Integrations.Cod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionState.OnReceivedData.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
connectionState.OnReceivedData.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
rconSocket.Close();
|
rconSocket.Close();
|
||||||
throw new NetworkException("Timed out receiving RCon response", rconSocket);
|
throw new NetworkException("Timed out receiving RCon response", rconSocket);
|
||||||
}
|
}
|
||||||
@ -526,7 +501,7 @@ namespace Integrations.Cod
|
|||||||
return GetResponseData(connectionState);
|
return GetResponseData(connectionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[][] GetResponseData(ConnectionState connectionState)
|
private static byte[][] GetResponseData(ConnectionState connectionState)
|
||||||
{
|
{
|
||||||
var responseList = new List<byte[]>();
|
var responseList = new List<byte[]>();
|
||||||
var totalBytesRead = 0;
|
var totalBytesRead = 0;
|
||||||
@ -571,7 +546,11 @@ namespace Integrations.Cod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sender is not Socket sock)
|
var state = ActiveQueries[Endpoint];
|
||||||
|
var cancellationRequested = ((ConnectionUserToken)e.UserToken)?.CancellationToken.IsCancellationRequested ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
if (sender is not Socket sock || cancellationRequested)
|
||||||
{
|
{
|
||||||
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
|
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
|
||||||
|
|
||||||
@ -591,7 +570,6 @@ namespace Integrations.Cod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = ActiveQueries[Endpoint];
|
|
||||||
state.BytesReadPerSegment.Add(e.BytesTransferred);
|
state.BytesReadPerSegment.Add(e.BytesTransferred);
|
||||||
|
|
||||||
// I don't even want to know why this works for getting more data from Cod4x
|
// I don't even want to know why this works for getting more data from Cod4x
|
||||||
@ -604,6 +582,7 @@ namespace Integrations.Cod
|
|||||||
var totalBytesTransferred = e.BytesTransferred;
|
var totalBytesTransferred = e.BytesTransferred;
|
||||||
_log.LogDebug("{Total} total bytes transferred with {Available} bytes remaining", totalBytesTransferred,
|
_log.LogDebug("{Total} total bytes transferred with {Available} bytes remaining", totalBytesTransferred,
|
||||||
sock.Available);
|
sock.Available);
|
||||||
|
|
||||||
// we still have available data so the payload was segmented
|
// we still have available data so the payload was segmented
|
||||||
while (sock.Available > 0)
|
while (sock.Available > 0)
|
||||||
{
|
{
|
||||||
@ -619,7 +598,6 @@ namespace Integrations.Cod
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
|
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
|
||||||
|
|
||||||
if (sock.ReceiveAsync(state.ReceiveEventArgs))
|
if (sock.ReceiveAsync(state.ReceiveEventArgs))
|
||||||
{
|
{
|
||||||
_log.LogDebug("Remaining bytes are async");
|
_log.LogDebug("Remaining bytes are async");
|
||||||
@ -628,6 +606,7 @@ namespace Integrations.Cod
|
|||||||
|
|
||||||
_log.LogDebug("Read {BytesTransferred} synchronous bytes from {Endpoint}",
|
_log.LogDebug("Read {BytesTransferred} synchronous bytes from {Endpoint}",
|
||||||
state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint?.ToString());
|
state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint?.ToString());
|
||||||
|
|
||||||
// we need to increment this here because the callback isn't executed if there's no pending IO
|
// we need to increment this here because the callback isn't executed if there's no pending IO
|
||||||
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
|
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
|
||||||
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
|
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
|
||||||
|
@ -24,9 +24,15 @@ namespace Integrations.Cod
|
|||||||
public readonly SemaphoreSlim OnSentData = new(1, 1);
|
public readonly SemaphoreSlim OnSentData = new(1, 1);
|
||||||
public readonly SemaphoreSlim OnReceivedData = new (1, 1);
|
public readonly SemaphoreSlim OnReceivedData = new (1, 1);
|
||||||
|
|
||||||
public List<int> BytesReadPerSegment { get; set; } = new List<int>();
|
public List<int> BytesReadPerSegment { get; set; } = new();
|
||||||
public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
|
public SocketAsyncEventArgs SendEventArgs { get; set; } = new();
|
||||||
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
|
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new();
|
||||||
public DateTime LastQuery { get; set; } = DateTime.Now;
|
public DateTime LastQuery { get; set; } = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class ConnectionUserToken
|
||||||
|
{
|
||||||
|
public Socket Socket { get; set; }
|
||||||
|
public CancellationToken CancellationToken { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user