async Task DialogSessionMainProcAsync(DialogSession session, CancellationToken cancel) { session.Debug("Start"); ThinClientConnectOptions connectOptions = (ThinClientConnectOptions)session.Param !; await using WideTunnel wt = new WideTunnel(this.Options.WideTunnelOptions); ThinClientAuthResponse authResponseCache = null !; session.Debug($"Begin DcConnectEx. connectOptions: {connectOptions._GetObjectDump()}"); await using ThinClientConnection firstConnection = await DcConnectEx(wt, connectOptions, cancel, true, true, async (req, c) => { // 認証コールバック var response = (ThinClientAuthResponse)(await session.RequestAndWaitResponseAsync(req, Consts.ThinClient.RequestHardTimeoutMsecs, Consts.ThinClient.RequestSoftTimeoutMsecs, cancel)); authResponseCache = response; return(response); }, async (req, c) => { // OTP コールバック var response = (ThinClientOtpResponse)(await session.RequestAndWaitResponseAsync(req, Consts.ThinClient.RequestHardTimeoutMsecs, Consts.ThinClient.RequestSoftTimeoutMsecs, cancel)); return(response); }, async (req, c) => { // 検疫・MAC アドレスコールバック var response = (ThinClientInspectResponse)(await session.RequestAndWaitResponseAsync(req, Consts.ThinClient.RequestHardTimeoutMsecs, Consts.ThinClient.RequestSoftTimeoutMsecs, cancel)); return(response); }); //if (connectOptions.DebugGuacMode == false) //{ // session.Debug("Switching to WebSocket mode..."); // string webSocketUrl = await firstConnection.Socket.RequestSwitchToWebSocketAsync(cancel, Consts.ThinClient.RequestSwitchToWebSocketTimeoutMsecs); // session.Debug($"Switching to WebSocket mode OK. webSocketUrl = {webSocketUrl}"); //} session.Debug("First WideTunnel Connected."); if (connectOptions.DebugGuacMode == false) { // ThinGate ネイティブ WebSocket モード (ThinGate が WebSocket を直接話す) if (firstConnection.Caps.Bit(ThinServerCaps.GuacdSupported) == false) { // 古いバージョンであり、サポートされていない throw new VpnException(VpnError.ERR_DESK_GUACD_NOT_SUPPORTED_VER); } // 'A' を送信 // この段階でサーバー機の Guacd にソケットが接続された状態となる byte[] a = new byte[] { (byte)'A' }; await firstConnection.Stream.SendAsync(a, cancel); // 最初に少し Guacamole Protocol で ThinWebClient と guacd との間で対話を行なう。 // これを行なうことにより、guacd が、local PC の RDP/VNC に接続し通信を初期化する。 // その後、WebSocket への切替えを行ない、それ以降は guacd とクライアント Web ブラウザが直接対話する。 await using var guaClient = new GuaClient( new GuaClientSettings( "", 0, firstConnection.SvcType == ThinSvcType.Rdp ? GuaProtocol.Rdp : GuaProtocol.Vnc, "127.0.0.1", firstConnection.SvcPort, connectOptions.GuaPreference !, connectOptions.IsWebpSupported, firstConnection.Caps.Bit(ThinServerCaps.AudioInSupported)), firstConnection.Stream); string webSocketUrl = ""; // この非同期 StartAsync は、Guacd Protocol で "select, rdp" のような最初の hello に相当するプロトコルを送付し、 // その後、最初の応答を受信してから、いくつかの追加パラメータを送付し、最後に、Connect パケットを送付する直前に呼び出される。 // Connect パケットの本体は文字列にして HTML5 クライアントに渡される。 // その後、HTML5 クライアントが WebSocket を ThinGate との間で確立した後に、 // HTML5 クライアントから最初のパケットとして Connect パケットの本体が渡されることになるのである。 string connectPacketData = await guaClient.StartAsync(cancel); webSocketUrl = await firstConnection.Socket.RequestSwitchToWebSocketAsync(cancel, Consts.ThinClient.RequestSwitchToWebSocketTimeoutMsecs); $"webSocketUrl = {webSocketUrl}"._Debug(); webSocketUrl._NotEmptyCheck(nameof(webSocketUrl)); // フル WebSocket URL を生成する var gateEndPoint = firstConnection.Socket.GatePhysicalEndPoint; // WebSocket URL を HTML クライアントに通知する var ready = new ThinClientAcceptReadyNotification { WebSocketFullUrl = "wss://" + IPUtil.GenerateWildCardDnsFqdn(gateEndPoint.Address, firstConnection.Socket.Options.ConnectParam.WebSocketWildCardDomainName, "ws-", "") + (gateEndPoint.Port == 443 ? "" : ":" + gateEndPoint.Port.ToString()) + webSocketUrl, SvcType = firstConnection.SvcType, ConnectPacketData = connectPacketData, WatermarkStr1 = firstConnection.WatermarkStr1, WatermarkStr2 = firstConnection.WatermarkStr2, Misc = firstConnection.Misc, Caps = firstConnection.Caps, IsStandaloneMode = firstConnection.Socket.Options.ConnectParam.IsStandaloneMode, }; if (firstConnection.Socket.Options.ConnectParam.WebSocketWildCardDomainName._IsSamei("<<!!samehost!!>>")) { ready.WebSocketFullUrl = webSocketUrl; } Dbg.Where(); await session.RequestAndWaitResponseAsync(ready, Consts.ThinClient.RequestHardTimeoutMsecs, Consts.ThinClient.RequestSoftTimeoutMsecs, isFinalAnswer : true); // 確立済みの firstConnection は何か 1 バイトでもデータを受信するか切断されるまで待機する // (このコネクションは ThinGate からみると用済みのため、新たなデータが届くことはないはずである) Dbg.Where("Waiting for unnecessary connection (already switched to WebSocket) disconnects."); // タイムアウトは 30 秒であります。 try { var data2 = await firstConnection.Stream._ReadAsyncWithTimeout(maxSize : 1024, timeout : 30 * 1000, cancel : cancel); } catch (Exception ex) { ex._Debug(); } Dbg.Where("Unnecessary connection (already switched to WebSocket) is disconnected."); } else { // Guacd Proxy Mode (デバッグ・開発用) await using var abort = new CancelWatcher(cancel); AsyncAutoResetEvent connectionAddedEvent = new AsyncAutoResetEvent(); AsyncAutoResetEvent connectedEvent = new AsyncAutoResetEvent(); Task? connectionKeepTask = null; NetTcpListener?listener = null; // このコネクションをキューに追加する ConcurrentQueue <ThinClientConnection> connectionQueue = new ConcurrentQueue <ThinClientConnection>(); try { connectionQueue.Enqueue(firstConnection); // コネクションのキューの長さが ConnectionQueueLength 未満になると自動的にコネクションを張る非同期タスクを開始する connectionKeepTask = TaskUtil.StartAsyncTaskAsync(async() => { int numRetry = 0; while (abort.IsCancellationRequested == false) { // キューの長さが少なくなれば追加コネクションを接続する while (abort.IsCancellationRequested == false) { if (connectionQueue.Count < Consts.ThinClient.ConnectionQueueLength) { session.Debug($"connectionQueue.Count ({connectionQueue.Count}) < Consts.ThinClient.ConnectionQueueLength ({Consts.ThinClient.ConnectionQueueLength})"); try { // 追加接続 ThinClientConnection additionalConnection = await DcConnectEx(wt, connectOptions, abort, false, false, async(req, c) => { // 認証コールバック await Task.CompletedTask; // キャッシュされた認証情報をそのまま応答 return(authResponseCache); }, async(req, c) => { // OTP コールバック await Task.CompletedTask; // OTP チケットを応答 return(new ThinClientOtpResponse { Otp = firstConnection.OtpTicket }); }, async(req, c) => { // 検疫・MAC コールバック await Task.CompletedTask; // チケットを応答 return(new ThinClientInspectResponse { Ticket = firstConnection.InspectTicket }); }); // 接続に成功したのでキューに追加 connectionQueue.Enqueue(additionalConnection); session.Debug($"Additional WideTunnel Connected. connectionQueue.Count = ({connectionQueue.Count})"); connectionAddedEvent.Set(true); numRetry = 0; } catch (Exception ex) { // 接続に失敗したので一定時間待ってリトライする ex._Debug(); session.Error(ex._GetObjectDump()); numRetry++; int waitTime = Util.GenRandIntervalWithRetry(Consts.ThinClient.ReconnectRetrySpanMsecs, numRetry, Consts.ThinClient.ReconnectRetrySpanMaxMsecs); if (waitTime == 0) { waitTime = 1; } session.Debug($"Additional tunnel establish failed. numRetry = {numRetry}. Wait for {waitTime} msecs..."); await connectedEvent.WaitOneAsync(waitTime, abort); } } else { break; } } if (abort.IsCancellationRequested) { break; } await connectedEvent.WaitOneAsync(1000, abort); } }); // Listen ソケットの開始 listener = LocalNet.CreateTcpListener(new TcpListenParam(isRandomPortMode: EnsureSpecial.Yes, async(listen, sock) => { session.Debug($"Listener ({sock.EndPointInfo._GetObjectDump()}) Accepted."); ThinClientConnection?connection = null; long giveupTick = TickNow + Consts.ThinClient.ConnectionQueueWaitTimeout; // キューにコネクションが貯まるまで待機する while (connectionQueue.TryDequeue(out connection) == false) { if (TickNow >= giveupTick) { session.Debug($"Listener ({sock.EndPointInfo._GetObjectDump()}): TickNow ({TickNow}) >= giveupTick ({giveupTick})"); return; } abort.ThrowIfCancellationRequested(); await connectionAddedEvent.WaitOneAsync(100, abort.CancelToken); } await using ThinClientConnection connection2 = connection; session.Debug($"Listener ({sock.EndPointInfo._GetObjectDump()}) Starting Relay Operation."); // 'A' を送信 byte[] a = new byte[] { (byte)'A' }; await connection.Stream.SendAsync(a, abort); var sockStream = sock.GetStream(true); RefLong totalBytes = new RefLong(); try { await Util.RelayDuplexStreamAsync(sockStream, connection.Stream, abort, totalBytes: totalBytes); } catch (Exception ex) { session.Debug($"Listener ({sock.EndPointInfo._GetObjectDump()}) RelayDuplexStreamAsync Exception = {ex._GetObjectDump()}"); } session.Debug($"Listener ({sock.EndPointInfo._GetObjectDump()}) Finished Relay Operation. Total Bytes = {totalBytes.Value}"); // RDP の場合はダミーデータを最後に流す if (connection.SvcType == ThinSvcType.Rdp) { byte[] dummySize = new byte[4096]; await sockStream.SendAsync(dummySize, abort); await Task.Delay(50, abort); } }, family: Options.ListenAddressFamily, address: Options.ListenAddress)); session.Debug($"Create Listener. Assigned Random Port = {listener.AssignedRandomPort}"); // 接続が完了いたしました var ready = new ThinClientAcceptReadyNotification { ListenEndPoint = new IPEndPoint(listener.AssignedRandomPort !.IPAddress, listener.AssignedRandomPort.Port), FirstConnection = firstConnection, Caps = firstConnection.Caps, }; Dbg.Where(); await session.RequestAndWaitResponseAsync(ready, Consts.ThinClient.RequestHardTimeoutMsecs, Consts.ThinClient.RequestSoftTimeoutMsecs, abort, isFinalAnswer : true); // コネクション数が 0 の状態が 30 秒以上継続した場合は終了します long connectionZeroStartTick = 0; while (true) { abort.ThrowIfCancellationRequested(); long now = TickNow; //$"listener.CurrentConnections = {listener.CurrentConnections}"._Debug(); if (listener.CurrentConnections == 0) { if (connectionZeroStartTick == 0) { connectionZeroStartTick = now; } } else { connectionZeroStartTick = 0; } if (connectionZeroStartTick != 0 && now >= (connectionZeroStartTick + Consts.ThinClient.ConnectionZeroTimeout)) { session.Debug($"All client connections disconnected. No more connection exists. connectionZeroStartTick = {connectionZeroStartTick}, now = {now}"); throw new CoresException("All client connections disconnected. No more connection exists."); } await abort.CancelToken._WaitUntilCanceledAsync(1000); } } catch (Exception ex) { session.Error(ex); throw; } finally { abort.Cancel(); await connectionKeepTask._TryAwait(noDebugMessage : true); await listener._DisposeSafeAsync(); foreach (var connection in connectionQueue) { await connection._DisposeSafeAsync(); } Dbg.Where(); } } }
public async Task <Result?> RunClientAsync() { CurrentCount.Increment(); try { if (Once.IsFirstCall() == false) { throw new ApplicationException("You cannot reuse the object."); } Con.WriteLine("Client mode start"); ExceptionQueue = new ExceptionQueue(); SessionId = Util.RandUInt64(); List <Task <Result> > tasks = new List <Task <Result> >(); List <AsyncManualResetEvent> readyEvents = new List <AsyncManualResetEvent>(); using (CancelWatcher cancelWatcher = new CancelWatcher(this.Cancel)) { for (int i = 0; i < NumConnection; i++) { Direction dir; if (Mode == SpeedTestModeFlag.Download) { dir = Direction.Recv; } else if (Mode == SpeedTestModeFlag.Upload) { dir = Direction.Send; } else { dir = ((i % 2) == 0) ? Direction.Recv : Direction.Send; } AsyncManualResetEvent readyEvent = new AsyncManualResetEvent(); var t = ClientSingleConnectionAsync(dir, readyEvent, cancelWatcher.CancelToken); ExceptionQueue.RegisterWatchedTask(t); tasks.Add(t); readyEvents.Add(readyEvent); } try { using (var whenAllReady = new WhenAll(readyEvents.Select(x => x.WaitAsync()))) { await TaskUtil.WaitObjectsAsync( tasks : tasks.Append(whenAllReady.WaitMe).ToArray(), cancels : cancelWatcher.CancelToken._SingleArray(), manualEvents : ExceptionQueue.WhenExceptionAdded._SingleArray()); } Cancel.ThrowIfCancellationRequested(); ExceptionQueue.ThrowFirstExceptionIfExists(); ExceptionQueue.WhenExceptionAdded.CallbackList.AddSoftCallback(x => { cancelWatcher.Cancel(); }); using (new DelayAction(TimeSpan * 3 + 180 * 1000, x => { cancelWatcher.Cancel(); }, doNotBlockOnDispose: true)) { ClientStartEvent.Set(true); using (var whenAllCompleted = new WhenAll(tasks)) { await TaskUtil.WaitObjectsAsync( tasks : whenAllCompleted.WaitMe._SingleArray(), cancels : cancelWatcher.CancelToken._SingleArray() ); await whenAllCompleted.WaitMe; } } Result ret = new Result(); ret.Span = TimeSpan; foreach (var r in tasks.Select(x => x._GetResult())) { ret.NumBytesDownload += r.NumBytesDownload; ret.NumBytesUpload += r.NumBytesUpload; } ret.NumBytesTotal = ret.NumBytesUpload + ret.NumBytesDownload; ret.BpsUpload = (long)((double)ret.NumBytesUpload * 1000.0 * 8.0 / (double)ret.Span * 1514.0 / 1460.0); ret.BpsDownload = (long)((double)ret.NumBytesDownload * 1000.0 * 8.0 / (double)ret.Span * 1514.0 / 1460.0); ret.BpsTotal = ret.BpsUpload + ret.BpsDownload; return(ret); } catch (Exception ex) { await Task.Yield(); ExceptionQueue.Add(ex); } finally { cancelWatcher.Cancel(); try { await Task.WhenAll(tasks); } catch { } } ExceptionQueue.ThrowFirstExceptionIfExists(); } return(null); } finally { CurrentCount.Decrement(); } }