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 }