Files
gott/Services/RemoteConnectionManager.cs
T
Meleki a13355d7e9 Remote: Server-seitiger Heartbeat mit Timeout für tote WebSocket-Verbindungen
- 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
2026-03-13 15:44:35 +01:00

148 lines
4.7 KiB
C#

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; }
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<RemoteCommandResult> 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<string, RemoteConnection> _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<RemoteConnection> 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;
}
/// <summary>
/// Sendet einen Befehl an den Remote-Client und wartet auf das Ergebnis.
/// </summary>
public async Task<RemoteCommandResult> ExecuteCommand(string connectionId, string command, int timeoutSeconds = 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<RemoteCommandResult>();
// 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);
// Auf Ergebnis warten (mit Timeout)
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
try
{
var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled());
var result = await conn.PendingResult.Task;
registration.Dispose();
return result;
}
catch (TaskCanceledException)
{
return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout nach {timeoutSeconds} Sekunden" };
}
}
finally
{
conn.PendingResult = null;
conn.CommandLock.Release();
}
}
/// <summary>
/// Wird vom WebSocket-Receiver aufgerufen wenn ein Ergebnis vom Client kommt.
/// </summary>
public void HandleResult(string connectionId, RemoteCommandResult result)
{
if (_connections.TryGetValue(connectionId, out var conn))
{
conn.PendingResult?.TrySetResult(result);
}
}
}