using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Gott.Services; public class RemoteConnection { public string Id { get; set; } public string Hostname { get; set; } public string Username { get; set; } public string OsVersion { get; set; } public DateTime ConnectedAt { get; set; } public DateTime LastActivityAt { get; set; } // Zeitpunkt der letzten Aktivität WÄHREND eines laufenden Befehls (progress/result). // Basis für den Inaktivitäts-Timeout in ExecuteCommand: Solange der Client periodisch // Progress-Frames sendet, darf ein Befehl beliebig lange laufen. public DateTime LastProgressAt { get; set; } public string AssignedSessionId { get; set; } public WebSocket Socket { get; set; } // Für synchrone Command-Execution: ein Befehl wird gesendet, Ergebnis abgewartet public SemaphoreSlim CommandLock { get; } = new SemaphoreSlim(1, 1); public TaskCompletionSource PendingResult { get; set; } } public class RemoteCommandResult { public int ExitCode { get; set; } public string Output { get; set; } public string Error { get; set; } } public class RemoteConnectionManager { private readonly ConcurrentDictionary _connections = new(); public RemoteConnection Register(WebSocket socket) { var conn = new RemoteConnection { Id = Guid.NewGuid().ToString("N")[..12], ConnectedAt = DateTime.UtcNow, LastActivityAt = DateTime.UtcNow, Socket = socket }; _connections[conn.Id] = conn; return conn; } public void Unregister(string connectionId) { if (_connections.TryRemove(connectionId, out var conn)) { // Falls ein Befehl auf Antwort wartet, abbrechen conn.PendingResult?.TrySetCanceled(); conn.AssignedSessionId = null; } } public List GetAll() { return _connections.Values.ToList(); } public RemoteConnection GetById(string connectionId) { _connections.TryGetValue(connectionId, out var conn); return conn; } public RemoteConnection GetBySessionId(string sessionId) { return _connections.Values.FirstOrDefault(c => c.AssignedSessionId == sessionId); } public bool Assign(string connectionId, string sessionId) { if (!_connections.TryGetValue(connectionId, out var conn)) return false; conn.AssignedSessionId = sessionId; return true; } public bool Unassign(string connectionId) { if (!_connections.TryGetValue(connectionId, out var conn)) return false; conn.AssignedSessionId = null; return true; } /// /// Sendet einen Befehl an den Remote-Client und wartet auf das Ergebnis. /// ist ein INAKTIVITÄTS-Timeout (kein hartes /// Wall-Clock-Limit): Der Befehl darf beliebig lange laufen, solange der Client periodisch /// Progress-Frames ({"type":"progress"}) sendet. Erst wenn für diese Dauer KEINE Aktivität /// (progress oder result) mehr kommt, wird abgebrochen. So laufen schwere Aufgaben nicht mehr /// in einen starren 180s-Timeout. /// public async Task ExecuteCommand(string connectionId, string command, int inactivityTimeoutSeconds = 180, CancellationToken ct = default) { if (!_connections.TryGetValue(connectionId, out var conn)) return new RemoteCommandResult { ExitCode = -1, Error = "Verbindung nicht gefunden" }; if (conn.Socket.State != WebSocketState.Open) return new RemoteCommandResult { ExitCode = -1, Error = "WebSocket nicht verbunden" }; await conn.CommandLock.WaitAsync(ct); try { conn.PendingResult = new TaskCompletionSource(); conn.LastProgressAt = DateTime.UtcNow; // Befehl senden var msg = JsonSerializer.Serialize(new { type = "execute", command }); var bytes = Encoding.UTF8.GetBytes(msg); await conn.Socket.SendAsync(bytes, WebSocketMessageType.Text, true, ct); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); using var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled()); // Watchdog: bricht nur ab, wenn seit inactivityTimeoutSeconds keine Progress-/ // Result-Aktivität mehr vom Client kam. Progress-Frames setzen conn.LastProgressAt // (siehe RemoteEndpoint.ReceiveLoop) und verlängern damit die Frist. var watchdog = Task.Run(async () => { try { while (!cts.IsCancellationRequested) { var idleSec = (DateTime.UtcNow - conn.LastProgressAt).TotalSeconds; var remaining = inactivityTimeoutSeconds - idleSec; if (remaining <= 0) { cts.Cancel(); return; } // Häufig genug prüfen, aber nicht busy-spinnen (max 5s Takt) await Task.Delay(TimeSpan.FromSeconds(Math.Min(remaining, 5)), cts.Token); } } catch (OperationCanceledException) { } }, cts.Token); try { var result = await conn.PendingResult.Task; return result; } catch (TaskCanceledException) { return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout: keine Aktivität vom Remote-Client für {inactivityTimeoutSeconds}s" }; } finally { cts.Cancel(); try { await watchdog; } catch { } } } finally { conn.PendingResult = null; conn.CommandLock.Release(); } } /// /// Wird vom WebSocket-Receiver aufgerufen wenn ein Progress-/Keep-Alive-Frame vom Client kommt. /// Setzt den Inaktivitäts-Zähler zurück, damit ein lang laufender Befehl nicht abgebrochen wird. /// public void HandleProgress(string connectionId) { if (_connections.TryGetValue(connectionId, out var conn)) conn.LastProgressAt = DateTime.UtcNow; } /// /// Wird vom WebSocket-Receiver aufgerufen wenn ein Ergebnis vom Client kommt. /// public void HandleResult(string connectionId, RemoteCommandResult result) { if (_connections.TryGetValue(connectionId, out var conn)) { conn.PendingResult?.TrySetResult(result); } } }