627 lines
14 KiB
C#
627 lines
14 KiB
C#
using System;
|
|||
|
|
using System.Buffers.Binary;
|
||
|
|
using System.Collections.Concurrent;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Net;
|
||
|
|
using System.Net.Quic;
|
||
|
|
using System.Net.Security;
|
||
|
|
using System.Security.Cryptography.X509Certificates;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Channels;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
namespace Crysome.Common.Network.Transport;
|
||
|
|
|
||
|
|
public sealed class QuicTransportSession : ITransportSession, IDisposable, IAsyncDisposable
|
||
|
|
{
|
||
|
|
private sealed class FragBuf
|
||
|
|
{
|
||
|
|
public byte[][] Frags;
|
||
|
|
|
||
|
|
public int RecvCount;
|
||
|
|
|
||
|
|
public bool IsComplete
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if (Frags != null)
|
||
|
|
{
|
||
|
|
return RecvCount >= Frags.Length;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public const string Alpn = "crysome";
|
||
|
|
|
||
|
|
private readonly QuicConnection _conn;
|
||
|
|
|
||
|
|
private readonly bool _isServer;
|
||
|
|
|
||
|
|
private readonly QuicStream _control;
|
||
|
|
|
||
|
|
private readonly QuicStream _screenOut;
|
||
|
|
|
||
|
|
private readonly QuicStream _screenIn;
|
||
|
|
|
||
|
|
private readonly QuicStream _bulkOut;
|
||
|
|
|
||
|
|
private readonly QuicStream _bulkIn;
|
||
|
|
|
||
|
|
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||
|
|
|
||
|
|
private readonly object _controlWriteLock = new object();
|
||
|
|
|
||
|
|
private readonly object _bulkWriteLock = new object();
|
||
|
|
|
||
|
|
private readonly object _screenWriteLock = new object();
|
||
|
|
|
||
|
|
private readonly Channel<byte[]> _screenChannel;
|
||
|
|
|
||
|
|
private volatile bool _alive = true;
|
||
|
|
|
||
|
|
private long _bytesSent;
|
||
|
|
|
||
|
|
private long _bytesRecv;
|
||
|
|
|
||
|
|
private double _rttMs = 100.0;
|
||
|
|
|
||
|
|
private double _lossRate;
|
||
|
|
|
||
|
|
private DateTime _lastCongestionEmit = DateTime.UtcNow;
|
||
|
|
|
||
|
|
private readonly ConcurrentDictionary<uint, FragBuf> _screenFrags = new ConcurrentDictionary<uint, FragBuf>();
|
||
|
|
|
||
|
|
private uint _screenLastSeq;
|
||
|
|
|
||
|
|
private readonly object _screenSeqLock = new object();
|
||
|
|
|
||
|
|
private bool _disposed;
|
||
|
|
|
||
|
|
public IPEndPoint RemoteEndPoint => _conn?.RemoteEndPoint;
|
||
|
|
|
||
|
|
public IPEndPoint LocalEndPoint => _conn?.LocalEndPoint;
|
||
|
|
|
||
|
|
public bool IsAlive
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if (_alive)
|
||
|
|
{
|
||
|
|
return !_disposed;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public double RttMs => _rttMs;
|
||
|
|
|
||
|
|
public double LossRate => _lossRate;
|
||
|
|
|
||
|
|
public long BytesSent => _bytesSent;
|
||
|
|
|
||
|
|
public long BytesRecv => _bytesRecv;
|
||
|
|
|
||
|
|
public event Action<byte[]> ReliableReceived;
|
||
|
|
|
||
|
|
public event Action<byte, byte[]> UnreliableFrameReceived;
|
||
|
|
|
||
|
|
public event Action<double> CongestionChanged;
|
||
|
|
|
||
|
|
public event Action Disconnected;
|
||
|
|
|
||
|
|
private QuicTransportSession(QuicConnection conn, bool isServer, QuicStream control, QuicStream screenOut, QuicStream screenIn, QuicStream bulkOut, QuicStream bulkIn)
|
||
|
|
{
|
||
|
|
_conn = conn;
|
||
|
|
_isServer = isServer;
|
||
|
|
_control = control;
|
||
|
|
_screenOut = screenOut;
|
||
|
|
_screenIn = screenIn;
|
||
|
|
_bulkOut = bulkOut;
|
||
|
|
_bulkIn = bulkIn;
|
||
|
|
_screenChannel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(1)
|
||
|
|
{
|
||
|
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||
|
|
SingleReader = true,
|
||
|
|
SingleWriter = false
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async ValueTask<QuicTransportSession> ConnectClientAsync(EndPoint remote, CancellationToken ct)
|
||
|
|
{
|
||
|
|
if (!QuicConnection.IsSupported)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException("QUIC is not supported on this OS or runtime build.");
|
||
|
|
}
|
||
|
|
QuicConnection conn = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions
|
||
|
|
{
|
||
|
|
RemoteEndPoint = remote,
|
||
|
|
DefaultCloseErrorCode = 0L,
|
||
|
|
DefaultStreamErrorCode = 0L,
|
||
|
|
MaxInboundBidirectionalStreams = 256,
|
||
|
|
MaxInboundUnidirectionalStreams = 256,
|
||
|
|
ClientAuthenticationOptions = new SslClientAuthenticationOptions
|
||
|
|
{
|
||
|
|
ApplicationProtocols = new List<SslApplicationProtocol>
|
||
|
|
{
|
||
|
|
new SslApplicationProtocol("crysome")
|
||
|
|
},
|
||
|
|
RemoteCertificateValidationCallback = (object _, X509Certificate? _, X509Chain? _, SslPolicyErrors _) => true,
|
||
|
|
TargetHost = "crysome"
|
||
|
|
}
|
||
|
|
}, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
QuicTransportSession quicTransportSession = new QuicTransportSession(conn, isServer: false, await conn.OpenOutboundStreamAsync(QuicStreamType.Bidirectional, ct).ConfigureAwait(continueOnCapturedContext: false), await conn.OpenOutboundStreamAsync(QuicStreamType.Unidirectional, ct).ConfigureAwait(continueOnCapturedContext: false), null, null, await conn.AcceptInboundStreamAsync(ct).ConfigureAwait(continueOnCapturedContext: false));
|
||
|
|
quicTransportSession.StartLoops();
|
||
|
|
return quicTransportSession;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async ValueTask<QuicTransportSession> AcceptServerAsync(QuicConnection conn, CancellationToken ct)
|
||
|
|
{
|
||
|
|
QuicTransportSession quicTransportSession = new QuicTransportSession(conn, isServer: true, await conn.AcceptInboundStreamAsync(ct).ConfigureAwait(continueOnCapturedContext: false), null, await conn.AcceptInboundStreamAsync(ct).ConfigureAwait(continueOnCapturedContext: false), await conn.OpenOutboundStreamAsync(QuicStreamType.Unidirectional, ct).ConfigureAwait(continueOnCapturedContext: false), null);
|
||
|
|
quicTransportSession.StartLoops();
|
||
|
|
return quicTransportSession;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async ValueTask<QuicListener> StartListenerAsync(int port, X509Certificate2 certificate, CancellationToken ct)
|
||
|
|
{
|
||
|
|
if (!QuicListener.IsSupported)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException("QUIC listener is not supported on this OS or runtime build.");
|
||
|
|
}
|
||
|
|
return await QuicListener.ListenAsync(new QuicListenerOptions
|
||
|
|
{
|
||
|
|
ListenEndPoint = new IPEndPoint(IPAddress.Any, port),
|
||
|
|
ListenBacklog = 512,
|
||
|
|
ApplicationProtocols = new List<SslApplicationProtocol>
|
||
|
|
{
|
||
|
|
new SslApplicationProtocol("crysome")
|
||
|
|
},
|
||
|
|
ConnectionOptionsCallback = (QuicConnection connection, SslClientHelloInfo clientHello, CancellationToken token) => ValueTask.FromResult(new QuicServerConnectionOptions
|
||
|
|
{
|
||
|
|
DefaultStreamErrorCode = 0L,
|
||
|
|
DefaultCloseErrorCode = 0L,
|
||
|
|
MaxInboundBidirectionalStreams = 256,
|
||
|
|
MaxInboundUnidirectionalStreams = 256,
|
||
|
|
ServerAuthenticationOptions = new SslServerAuthenticationOptions
|
||
|
|
{
|
||
|
|
ApplicationProtocols = new List<SslApplicationProtocol>
|
||
|
|
{
|
||
|
|
new SslApplicationProtocol("crysome")
|
||
|
|
},
|
||
|
|
ServerCertificate = certificate
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void StartLoops()
|
||
|
|
{
|
||
|
|
CancellationToken ct = _cts.Token;
|
||
|
|
Task.Run(() => ControlReadLoop(ct), ct);
|
||
|
|
Task.Run(() => CongestionEmitLoop(ct), ct);
|
||
|
|
if (_isServer && _screenIn != null)
|
||
|
|
{
|
||
|
|
Task.Run(() => ScreenReadLoop(ct), ct);
|
||
|
|
}
|
||
|
|
if (!_isServer && _bulkIn != null)
|
||
|
|
{
|
||
|
|
Task.Run(() => BulkReadLoop(ct), ct);
|
||
|
|
}
|
||
|
|
if (!_isServer && _screenOut != null)
|
||
|
|
{
|
||
|
|
Task.Run(() => ScreenWriterLoop(ct), ct);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task CongestionEmitLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
while (!ct.IsCancellationRequested && _alive)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await Task.Delay(2000, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
EmitCongestion();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void EmitCongestion()
|
||
|
|
{
|
||
|
|
DateTime utcNow = DateTime.UtcNow;
|
||
|
|
if ((utcNow - _lastCongestionEmit).TotalMilliseconds < 500.0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_lastCongestionEmit = utcNow;
|
||
|
|
double num = Math.Min(1.0, _rttMs / 2000.0);
|
||
|
|
double lossRate = _lossRate;
|
||
|
|
double obj = Math.Min(1.0, num * 0.5 + lossRate * 0.5);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.CongestionChanged?.Invoke(obj);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SendReliable(ReadOnlySpan<byte> payload)
|
||
|
|
{
|
||
|
|
if (_disposed || !_alive || _control == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
lock (_controlWriteLock)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
WriteFrameSync(_control, payload);
|
||
|
|
Interlocked.Add(ref _bytesSent, 4 + payload.Length);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
TriggerDisconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SendReliableBulk(ReadOnlySpan<byte> payload)
|
||
|
|
{
|
||
|
|
if (!_isServer || _bulkOut == null || _disposed || !_alive)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
lock (_bulkWriteLock)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
WriteFrameSync(_bulkOut, payload);
|
||
|
|
Interlocked.Add(ref _bytesSent, 4 + payload.Length);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
TriggerDisconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SendPacketUnreliable(ReadOnlySpan<byte> payload)
|
||
|
|
{
|
||
|
|
SendReliable(payload);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SendUnreliableScreen(byte packetTypeId, ReadOnlySpan<byte> payload)
|
||
|
|
{
|
||
|
|
if (!_isServer && _screenOut != null)
|
||
|
|
{
|
||
|
|
uint frameSeq;
|
||
|
|
lock (_screenSeqLock)
|
||
|
|
{
|
||
|
|
frameSeq = ++_screenLastSeq;
|
||
|
|
}
|
||
|
|
byte[] array = payload.ToArray();
|
||
|
|
int num = Math.Max(1, (array.Length + 60000 - 1) / 60000);
|
||
|
|
for (ushort num2 = 0; num2 < num; num2++)
|
||
|
|
{
|
||
|
|
int num3 = num2 * 60000;
|
||
|
|
int payloadLength = Math.Min(60000, array.Length - num3);
|
||
|
|
byte[] item = UdpTransport.BuildFragment(0u, packetTypeId, frameSeq, num2, (ushort)num, array, num3, payloadLength);
|
||
|
|
_screenChannel.Writer.TryWrite(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ScreenWriterLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
_ = 1;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await foreach (byte[] item in _screenChannel.Reader.ReadAllAsync(ct).ConfigureAwait(continueOnCapturedContext: false))
|
||
|
|
{
|
||
|
|
if (_disposed || !_alive)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
lock (_screenWriteLock)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
WriteFrameSync(_screenOut, item);
|
||
|
|
Interlocked.Add(ref _bytesSent, 4 + item.Length);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void WriteFrameSync(QuicStream stream, ReadOnlySpan<byte> payload)
|
||
|
|
{
|
||
|
|
byte[] array = new byte[4];
|
||
|
|
BinaryPrimitives.WriteInt32LittleEndian(array, payload.Length);
|
||
|
|
stream.Write(array);
|
||
|
|
stream.Write(payload);
|
||
|
|
stream.Flush();
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ControlReadLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
while (!ct.IsCancellationRequested && _alive)
|
||
|
|
{
|
||
|
|
byte[] array = await ReadFrameAsync(_control, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
Interlocked.Add(ref _bytesRecv, 4 + array.Length);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.ReliableReceived?.Invoke(array);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
TriggerDisconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ScreenReadLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
while (!ct.IsCancellationRequested && _alive)
|
||
|
|
{
|
||
|
|
byte[] array = await ReadFrameAsync(_screenIn, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
Interlocked.Add(ref _bytesRecv, 4 + array.Length);
|
||
|
|
ProcessScreenDatagram(array);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
TriggerDisconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ProcessScreenDatagram(byte[] dg)
|
||
|
|
{
|
||
|
|
if (!UdpTransport.ParseHeader(dg, dg.Length, out var _, out var packetTypeId, out var frameSeq, out var fragIdx, out var fragCount, out var payloadOffset, out var payloadLength))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (fragCount <= 1)
|
||
|
|
{
|
||
|
|
byte[] array = new byte[payloadLength];
|
||
|
|
if (payloadLength > 0)
|
||
|
|
{
|
||
|
|
Buffer.BlockCopy(dg, payloadOffset, array, 0, payloadLength);
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.UnreliableFrameReceived?.Invoke(packetTypeId, array);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
byte[] array2 = new byte[payloadLength];
|
||
|
|
if (payloadLength > 0)
|
||
|
|
{
|
||
|
|
Buffer.BlockCopy(dg, payloadOffset, array2, 0, payloadLength);
|
||
|
|
}
|
||
|
|
FragBuf orAdd = _screenFrags.GetOrAdd(frameSeq, (uint _) => new FragBuf
|
||
|
|
{
|
||
|
|
Frags = new byte[fragCount][]
|
||
|
|
});
|
||
|
|
if (fragIdx >= orAdd.Frags.Length || orAdd.Frags[fragIdx] != null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
orAdd.Frags[fragIdx] = array2;
|
||
|
|
Interlocked.Increment(ref orAdd.RecvCount);
|
||
|
|
if (!orAdd.IsComplete)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_screenFrags.TryRemove(frameSeq, out var _);
|
||
|
|
int num = 0;
|
||
|
|
byte[][] frags = orAdd.Frags;
|
||
|
|
foreach (byte[] array3 in frags)
|
||
|
|
{
|
||
|
|
if (array3 != null)
|
||
|
|
{
|
||
|
|
num += array3.Length;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
byte[] array4 = new byte[num];
|
||
|
|
int num3 = 0;
|
||
|
|
frags = orAdd.Frags;
|
||
|
|
foreach (byte[] array5 in frags)
|
||
|
|
{
|
||
|
|
if (array5 != null)
|
||
|
|
{
|
||
|
|
Buffer.BlockCopy(array5, 0, array4, num3, array5.Length);
|
||
|
|
num3 += array5.Length;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.UnreliableFrameReceived?.Invoke(packetTypeId, array4);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task BulkReadLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
while (!ct.IsCancellationRequested && _alive)
|
||
|
|
{
|
||
|
|
byte[] array = await ReadFrameAsync(_bulkIn, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
Interlocked.Add(ref _bytesRecv, 4 + array.Length);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.ReliableReceived?.Invoke(array);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
TriggerDisconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task<byte[]> ReadFrameAsync(QuicStream stream, CancellationToken ct)
|
||
|
|
{
|
||
|
|
byte[] lenBuf = new byte[4];
|
||
|
|
await ReadExactAsync(stream, lenBuf, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
int num = BinaryPrimitives.ReadInt32LittleEndian(lenBuf);
|
||
|
|
if (num < 0 || num > 104857600)
|
||
|
|
{
|
||
|
|
throw new IOException("Invalid frame length");
|
||
|
|
}
|
||
|
|
byte[] buf = new byte[num];
|
||
|
|
await ReadExactAsync(stream, buf, ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
return buf;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task ReadExactAsync(QuicStream stream, Memory<byte> buffer, CancellationToken ct)
|
||
|
|
{
|
||
|
|
int num;
|
||
|
|
for (int o = 0; o < buffer.Length; o += num)
|
||
|
|
{
|
||
|
|
num = await stream.ReadAsync(buffer.Slice(o), ct).ConfigureAwait(continueOnCapturedContext: false);
|
||
|
|
if (num == 0)
|
||
|
|
{
|
||
|
|
throw new IOException("Stream ended");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SendFin()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_cts.Cancel();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
_alive = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void TriggerDisconnect()
|
||
|
|
{
|
||
|
|
if (_disposed)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_alive = false;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_cts.Cancel();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
this.Disconnected?.Invoke();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
if (_disposed)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_disposed = true;
|
||
|
|
_alive = false;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_cts.Cancel();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_screenChannel.Writer.TryComplete();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_control?.Dispose();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_screenOut?.Dispose();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_screenIn?.Dispose();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_bulkOut?.Dispose();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_bulkIn?.Dispose();
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_conn?.DisposeAsync().AsTask().Wait(2000);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async ValueTask DisposeAsync()
|
||
|
|
{
|
||
|
|
Dispose();
|
||
|
|
await Task.CompletedTask;
|
||
|
|
}
|
||
|
|
}
|