From a13355d7e9ee8a71569ee0435c72ceb8e9834600 Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 13 Mar 2026 15:44:35 +0100 Subject: [PATCH] =?UTF-8?q?Remote:=20Server-seitiger=20Heartbeat=20mit=20T?= =?UTF-8?q?imeout=20f=C3=BCr=20tote=20WebSocket-Verbindungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Server sendet alle 30s Ping an Client, Client antwortet mit Pong - LastActivityAt wird bei jeder empfangenen Nachricht aktualisiert - Nach 90s Inaktivität wird Verbindung als tot erkannt und aufgeräumt - HeartbeatLoop läuft parallel zum ReceiveLoop - remote.ps1: ping-Handler hinzugefügt --- Endpoints/RemoteEndpoint.cs | 59 ++++++++++++++++++++++++++++- Services/RemoteConnectionManager.cs | 2 + wwwroot/remote.ps1 | 4 ++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Endpoints/RemoteEndpoint.cs b/Endpoints/RemoteEndpoint.cs index ffd1273..7c90d2d 100644 --- a/Endpoints/RemoteEndpoint.cs +++ b/Endpoints/RemoteEndpoint.cs @@ -50,9 +50,20 @@ public static class RemoteEndpoint using var ws = await ctx.WebSockets.AcceptWebSocketAsync(); var conn = mgr.Register(ws); + using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + try { - await ReceiveLoop(ws, conn, mgr, ct); + var receiveTask = ReceiveLoop(ws, conn, mgr, heartbeatCts.Token); + var heartbeatTask = HeartbeatLoop(ws, conn, heartbeatCts); + + // Warten bis einer von beiden endet (Receive-Fehler oder Heartbeat-Timeout) + await Task.WhenAny(receiveTask, heartbeatTask); + heartbeatCts.Cancel(); + + // Beide Tasks sauber abwarten + try { await receiveTask; } catch { } + try { await heartbeatTask; } catch { } } catch (WebSocketException) { @@ -104,6 +115,8 @@ public static class RemoteEndpoint try { msg = JsonSerializer.Deserialize(json); } catch { continue; } + conn.LastActivityAt = DateTime.UtcNow; + var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : ""; switch (type) @@ -129,10 +142,54 @@ public static class RemoteEndpoint var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}"); await ws.SendAsync(pong, WebSocketMessageType.Text, true, ct); break; + + case "pong": + // Antwort auf Server-Ping → LastActivityAt ist schon gesetzt + break; } } } + private static async Task HeartbeatLoop(WebSocket ws, RemoteConnection conn, CancellationTokenSource heartbeatCts) + { + const int pingIntervalSeconds = 30; + const int deadTimeoutSeconds = 90; + var pingBytes = Encoding.UTF8.GetBytes("{\"type\":\"ping\"}"); + + try + { + while (!heartbeatCts.IsCancellationRequested && ws.State == WebSocketState.Open) + { + await Task.Delay(TimeSpan.FromSeconds(pingIntervalSeconds), heartbeatCts.Token); + + // Prüfe ob der Client noch lebt + var silenceSec = (DateTime.UtcNow - conn.LastActivityAt).TotalSeconds; + if (silenceSec > deadTimeoutSeconds) + { + Console.WriteLine($"[Remote] Heartbeat timeout ({silenceSec:F0}s): {conn.Id} ({conn.Hostname})"); + heartbeatCts.Cancel(); + return; + } + + // Ping senden + try + { + await ws.SendAsync(pingBytes, WebSocketMessageType.Text, true, heartbeatCts.Token); + } + catch + { + // Send fehlgeschlagen → Verbindung tot + heartbeatCts.Cancel(); + return; + } + } + } + catch (OperationCanceledException) + { + // Normal – ReceiveLoop hat beendet + } + } + private static IResult GetConnections(RemoteConnectionManager mgr) { var connections = mgr.GetAll().Select(c => new diff --git a/Services/RemoteConnectionManager.cs b/Services/RemoteConnectionManager.cs index b86a315..bc0e0fd 100644 --- a/Services/RemoteConnectionManager.cs +++ b/Services/RemoteConnectionManager.cs @@ -17,6 +17,7 @@ public class RemoteConnection public string Username { get; set; } public string OsVersion { get; set; } public DateTime ConnectedAt { get; set; } + public DateTime LastActivityAt { get; set; } public string AssignedSessionId { get; set; } public WebSocket Socket { get; set; } @@ -42,6 +43,7 @@ public class RemoteConnectionManager { Id = Guid.NewGuid().ToString("N")[..12], ConnectedAt = DateTime.UtcNow, + LastActivityAt = DateTime.UtcNow, Socket = socket }; _connections[conn.Id] = conn; diff --git a/wwwroot/remote.ps1 b/wwwroot/remote.ps1 index 173a4b1..bdbb00d 100644 --- a/wwwroot/remote.ps1 +++ b/wwwroot/remote.ps1 @@ -153,6 +153,10 @@ try { Write-Host " [Exit: $exitCode]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" }) Write-Host "" } + "ping" { + # Server-Heartbeat → mit pong antworten + Send-WsMessage $ws '{"type":"pong"}' $ct + } "pong" { # Heartbeat-Antwort - ignorieren }