From 3ea28def4cabb1c745eb669e41e093a2f174d3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Fri, 19 Jun 2026 12:06:50 +0200 Subject: [PATCH] =?UTF-8?q?Add:=20Dauerhaftes=20Bildschirm-=C3=9Cbertragun?= =?UTF-8?q?g-Overlay=20w=C3=A4hrend=20RDP-Session=20(v2.6.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RdpActiveIndicatorWindow: TeamViewer-Style Overlay unten rechts, solange RDP-Capture läuft. Zeigt Dauer der Sitzung + pulsierenden roten Punkt - User kann per "Trennen"-Button selbst die Sitzung beenden (TCP-Signal an Service, cancelt Capture-Loop sofort — auch nach Screen-Wechsel) - Overlay läuft als separater User-Prozess via SessionSpawner, wird vom Service direkt gekillt wenn Sitzung endet (rdp_stop oder Disconnect) - Version: 2.5.0 → 2.6.0 Co-Authored-By: Claude Sonnet 4.6 --- agent-cs/AgentWorker.cs | 2 +- agent-cs/IT-Nexus-Agent.csproj | 4 +- agent-cs/IndicatorModeRunner.cs | 20 +++++++ agent-cs/Program.cs | 4 ++ agent-cs/Services/RtcService.cs | 63 +++++++++++++++++++- agent-cs/UI/RdpActiveIndicatorWindow.xaml | 57 ++++++++++++++++++ agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs | 55 +++++++++++++++++ agent-cs/setup.iss | 2 +- frontend/src/pages/PatchManagementPage.jsx | 2 +- 9 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 agent-cs/IndicatorModeRunner.cs create mode 100644 agent-cs/UI/RdpActiveIndicatorWindow.xaml create mode 100644 agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs index 1f44a89..ba453e5 100644 --- a/agent-cs/AgentWorker.cs +++ b/agent-cs/AgentWorker.cs @@ -6,7 +6,7 @@ namespace ITNexusAgent; public class AgentWorker { - private const string Version = "2.5.0"; + private const string Version = "2.6.0"; private const string DataDir = @"C:\ProgramData\IT Nexus Agent"; private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json"; private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json"; diff --git a/agent-cs/IT-Nexus-Agent.csproj b/agent-cs/IT-Nexus-Agent.csproj index 8a9fc34..41454f7 100644 --- a/agent-cs/IT-Nexus-Agent.csproj +++ b/agent-cs/IT-Nexus-Agent.csproj @@ -7,8 +7,8 @@ true IT-Nexus-Agent ITNexusAgent - 2.5.0 - 2.5.0.0 + 2.6.0 + 2.6.0.0 enable enable false diff --git a/agent-cs/IndicatorModeRunner.cs b/agent-cs/IndicatorModeRunner.cs new file mode 100644 index 0000000..11fd3a1 --- /dev/null +++ b/agent-cs/IndicatorModeRunner.cs @@ -0,0 +1,20 @@ +using ITNexusAgent.UI; + +namespace ITNexusAgent; + +// Läuft als User-Prozess (via SessionSpawner), zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay +// solange RDP-Session aktiv ist. User kann selbst trennen (TCP-Signal an Service). +public static class IndicatorModeRunner +{ + public static void Run(string portStr) + { + if (!int.TryParse(portStr, out var port) || port <= 0) return; + + var app = new System.Windows.Application(); + app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose; + + var win = new RdpActiveIndicatorWindow(port); + win.Show(); + app.Run(); + } +} diff --git a/agent-cs/Program.cs b/agent-cs/Program.cs index 1a16e6a..c036b43 100644 --- a/agent-cs/Program.cs +++ b/agent-cs/Program.cs @@ -29,6 +29,10 @@ internal class Program args.Length > 2 && int.TryParse(args[2], out var si) ? si : 0); return; + case "--rdp-indicator": + IndicatorModeRunner.Run(args.Length > 1 ? args[1] : ""); + return; + case "--dashboard": RunDashboard(); return; diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs index 356c630..5095e4c 100644 --- a/agent-cs/Services/RtcService.cs +++ b/agent-cs/Services/RtcService.cs @@ -15,6 +15,9 @@ public class RtcService private readonly string _serverUrl; private readonly string _agentKey; private readonly string _hostname; + private int _indicatorPid = -1; + private TcpListener? _indicatorListener; + private CancellationTokenSource? _userDisconnectCts; public RtcService(string serverUrl, string agentKey, string hostname) { @@ -76,21 +79,39 @@ public class RtcService var newScreen = obj["screen"]?.ToObject() ?? 0; captureCts.Cancel(); captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct); - _ = CapturePipeLoopAsync(ws, newScreen, captureCts.Token); + var switchToken = _userDisconnectCts != null + ? CancellationTokenSource.CreateLinkedTokenSource(captureCts.Token, _userDisconnectCts.Token).Token + : captureCts.Token; + _ = CapturePipeLoopAsync(ws, newScreen, switchToken); AgentWorker.Log($"RDP: Screen gewechselt zu {newScreen}"); } else if (type == "rdp_stop" && captureCts != null) { captureCts.Cancel(); captureCts = null; + StopIndicator(); AgentWorker.Log("RDP: Screen-Capture gestoppt"); } } captureCts?.Cancel(); + StopIndicator(); AgentWorker.Log("RDP: Getrennt"); } + private void StopIndicator() + { + _userDisconnectCts?.Cancel(); + _userDisconnectCts = null; + try { _indicatorListener?.Stop(); } catch { } + _indicatorListener = null; + if (_indicatorPid > 0) + { + try { Process.GetProcessById(_indicatorPid).Kill(); } catch { } + _indicatorPid = -1; + } + } + private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct) { var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName; @@ -136,7 +157,45 @@ public class RtcService } AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture"); - await CapturePipeLoopAsync(ws, screenIdx, ct); + SpawnIndicator(exePath); + + var combinedToken = _userDisconnectCts != null + ? CancellationTokenSource.CreateLinkedTokenSource(ct, _userDisconnectCts.Token).Token + : ct; + await CapturePipeLoopAsync(ws, screenIdx, combinedToken); + } + + // Zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay (TeamViewer-Style) solange Capture läuft. + // User kann per Klick selbst trennen — Signal landet hier als CancellationTokenSource.Cancel(). + private void SpawnIndicator(string exePath) + { + try + { + _indicatorListener = new TcpListener(IPAddress.Loopback, 0); + _indicatorListener.Start(); + var port = ((IPEndPoint)_indicatorListener.LocalEndpoint).Port; + + _userDisconnectCts = new CancellationTokenSource(); + var listener = _indicatorListener; + var disconnectCts = _userDisconnectCts; + _ = Task.Run(async () => + { + try + { + using var tcp = await listener.AcceptTcpClientAsync(); + tcp.GetStream().ReadByte(); + disconnectCts.Cancel(); + AgentWorker.Log("RDP: User hat über Overlay getrennt"); + } + catch { } + }); + + _indicatorPid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-indicator {port}"); + } + catch (Exception ex) + { + AgentWorker.Log($"RDP: Indicator-Start fehlgeschlagen: {ex.Message}"); + } } private static bool SpawnConsentHelper(string exePath, string portStr) diff --git a/agent-cs/UI/RdpActiveIndicatorWindow.xaml b/agent-cs/UI/RdpActiveIndicatorWindow.xaml new file mode 100644 index 0000000..5eaba6d --- /dev/null +++ b/agent-cs/UI/RdpActiveIndicatorWindow.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs b/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs new file mode 100644 index 0000000..7036a7f --- /dev/null +++ b/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs @@ -0,0 +1,55 @@ +using System.Net.Sockets; +using System.Windows; +using System.Windows.Media.Animation; + +namespace ITNexusAgent.UI; + +public partial class RdpActiveIndicatorWindow : Window +{ + private readonly int _port; + private readonly DateTime _startedAt = DateTime.Now; + private readonly System.Windows.Threading.DispatcherTimer _timer = new(); + private bool _signaled = false; + + public RdpActiveIndicatorWindow(int port) + { + InitializeComponent(); + _port = port; + + Loaded += (s, e) => + { + var area = SystemParameters.WorkArea; + Left = area.Right - Width - 16; + Top = area.Bottom - Height - 16; + ((Storyboard)Resources["PulseAnim"]).Begin(PulseDot); + }; + + _timer.Interval = TimeSpan.FromSeconds(1); + _timer.Tick += (s, e) => + { + var elapsed = DateTime.Now - _startedAt; + DurationText.Text = $"IT-Abteilung sieht zu · {elapsed:mm\\:ss}"; + }; + _timer.Start(); + } + + private void DisconnectButton_Click(object sender, RoutedEventArgs e) + { + SendDisconnectSignal(); + Close(); + } + + private void SendDisconnectSignal() + { + if (_signaled) return; + _signaled = true; + try + { + using var tcp = new TcpClient(); + tcp.Connect("127.0.0.1", _port); + tcp.GetStream().WriteByte(1); + tcp.GetStream().Flush(); + } + catch { } + } +} diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss index 3e91bde..028f2f0 100644 --- a/agent-cs/setup.iss +++ b/agent-cs/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "IT Nexus Agent" -#define MyAppVersion "2.5.0" +#define MyAppVersion "2.6.0" #define MyAppPublisher "Cereda Systems GmbH" #define MyAppURL "https://it-nexus.cereda-systems.de" #define MyAppExeName "IT-Nexus-Agent.exe" diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx index cc187c5..dd36158 100644 --- a/frontend/src/pages/PatchManagementPage.jsx +++ b/frontend/src/pages/PatchManagementPage.jsx @@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext'; const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } }; -const LATEST_AGENT_VERSION = '2.5.0'; +const LATEST_AGENT_VERSION = '2.6.0'; const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' }; const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' }; const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };