From 058f968af1e33bbc8550de9f56e6fa5c4910c42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Thu, 11 Jun 2026 10:15:00 +0200 Subject: [PATCH] Add Remote Desktop screen selector + new-tab button (v2.2.0) - CaptureModeRunner: screen index param (0=all, 1-N=specific monitor) - RtcService: reads screen from rdp_start JSON, passes to capture helper - Program.cs: parses optional screen index arg for --rdp-capture - RemoteDesktopPanel: screen dropdown (Alle/1-4), new-tab button - RdpPage: fullscreen standalone page at /rdp/:agentId - AgentDetailPage: removed dead _RemoteDesktopOld_unused code Co-Authored-By: Claude Sonnet 4.6 --- agent-cs/CaptureModeRunner.cs | 27 +-- agent-cs/Program.cs | 4 +- agent-cs/Services/RtcService.cs | 13 +- .../components/common/RemoteDesktopPanel.jsx | 170 ++++++++++++++++++ frontend/src/pages/AgentDetailPage.jsx | 130 +------------- frontend/src/pages/RdpPage.jsx | 28 +++ frontend/src/routes.jsx | 3 + 7 files changed, 231 insertions(+), 144 deletions(-) create mode 100644 frontend/src/components/common/RemoteDesktopPanel.jsx create mode 100644 frontend/src/pages/RdpPage.jsx diff --git a/agent-cs/CaptureModeRunner.cs b/agent-cs/CaptureModeRunner.cs index dd3055f..1e9dc20 100644 --- a/agent-cs/CaptureModeRunner.cs +++ b/agent-cs/CaptureModeRunner.cs @@ -11,7 +11,7 @@ public static class CaptureModeRunner private static readonly ImageCodecInfo JpegCodec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid); - public static void Run(string portStr) + public static void Run(string portStr, int screenIdx = 0) { if (!int.TryParse(portStr, out var port) || port <= 0) return; @@ -24,22 +24,29 @@ public static class CaptureModeRunner var encParams = new EncoderParameters(1); encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L); - // Alle Screens zusammenfΓΌhren (Multi-Monitor-Support) + // Capture-Region bestimmen: 0 = alle Screens, 1-N = spezifischer Screen var allScreens = System.Windows.Forms.Screen.AllScreens; - int left = allScreens.Min(s => s.Bounds.X); - int top = allScreens.Min(s => s.Bounds.Y); - int right = allScreens.Max(s => s.Bounds.X + s.Bounds.Width); - int bottom = allScreens.Max(s => s.Bounds.Y + s.Bounds.Height); - int totalW = right - left; - int totalH = bottom - top; + Rectangle captureRect; + if (screenIdx <= 0 || screenIdx > allScreens.Length) + { + int left = allScreens.Min(s => s.Bounds.X); + int top = allScreens.Min(s => s.Bounds.Y); + int right = allScreens.Max(s => s.Bounds.X + s.Bounds.Width); + int bottom = allScreens.Max(s => s.Bounds.Y + s.Bounds.Height); + captureRect = new Rectangle(left, top, right - left, bottom - top); + } + else + { + captureRect = allScreens[screenIdx - 1].Bounds; + } while (tcp.Connected) { try { - using var bmp = new Bitmap(totalW, totalH, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using var bmp = new Bitmap(captureRect.Width, captureRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(bmp)) - g.CopyFromScreen(left, top, 0, 0, new Size(totalW, totalH)); + g.CopyFromScreen(captureRect.X, captureRect.Y, 0, 0, new Size(captureRect.Width, captureRect.Height)); byte[] jpeg; using (var ms = new MemoryStream()) diff --git a/agent-cs/Program.cs b/agent-cs/Program.cs index d6a94aa..bbe3d78 100644 --- a/agent-cs/Program.cs +++ b/agent-cs/Program.cs @@ -20,7 +20,9 @@ internal class Program return; case "--rdp-capture": - CaptureModeRunner.Run(args.Length > 1 ? args[1] : ""); + CaptureModeRunner.Run( + args.Length > 1 ? args[1] : "", + args.Length > 2 && int.TryParse(args[2], out var si) ? si : 0); return; case "--dashboard": diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs index 5a18ae4..e83d5f5 100644 --- a/agent-cs/Services/RtcService.cs +++ b/agent-cs/Services/RtcService.cs @@ -65,9 +65,10 @@ public class RtcService if (type == "rdp_start" && captureCts == null) { + var screenIdx = obj["screen"]?.ToObject() ?? 0; captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct); - _ = CapturePipeLoopAsync(ws, captureCts.Token); - AgentWorker.Log("RDP: Screen-Capture gestartet"); + _ = CapturePipeLoopAsync(ws, screenIdx, captureCts.Token); + AgentWorker.Log($"RDP: Screen-Capture gestartet (screen={screenIdx})"); } else if (type == "rdp_stop" && captureCts != null) { @@ -81,7 +82,7 @@ public class RtcService AgentWorker.Log("RDP: Getrennt"); } - private async Task CapturePipeLoopAsync(ClientWebSocket ws, CancellationToken ct) + private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct) { var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName; @@ -90,7 +91,7 @@ public class RtcService listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; - if (!SpawnCaptureHelper(exePath, port.ToString())) + if (!SpawnCaptureHelper(exePath, port.ToString(), screenIdx)) { listener.Stop(); AgentWorker.Log("RDP: Helper-Start fehlgeschlagen"); @@ -160,7 +161,7 @@ public class RtcService AgentWorker.Log("RDP: Frame-Loop beendet"); } - private static bool SpawnCaptureHelper(string exePath, string portStr) + private static bool SpawnCaptureHelper(string exePath, string portStr, int screenIdx = 0) { try { @@ -181,7 +182,7 @@ public class RtcService ? "/ru \"INTERACTIVE\"" : $"/ru \"{fullUser}\""; - var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {portStr}\" " + + var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {portStr} {screenIdx}\" " + $"/sc ONCE /st {triggerTime} {ruArg} /it /f"; var p = Process.Start(new ProcessStartInfo("schtasks.exe", args) diff --git a/frontend/src/components/common/RemoteDesktopPanel.jsx b/frontend/src/components/common/RemoteDesktopPanel.jsx new file mode 100644 index 0000000..f2a1385 --- /dev/null +++ b/frontend/src/components/common/RemoteDesktopPanel.jsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect, useRef } from 'react'; + +const SCREEN_OPTIONS = [ + { value: 0, label: 'Alle Bildschirme' }, + { value: 1, label: 'Bildschirm 1' }, + { value: 2, label: 'Bildschirm 2' }, + { value: 3, label: 'Bildschirm 3' }, + { value: 4, label: 'Bildschirm 4' }, +]; + +export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect = false, fullscreen = false }) { + const [status, setStatus] = useState('idle'); + const [error, setError] = useState(''); + const [fps, setFps] = useState(0); + const [resolution, setResolution] = useState(''); + const [screenIdx, setScreenIdx] = useState(0); + const canvasRef = useRef(null); + const wsRef = useRef(null); + const fpsRef = useRef({ count: 0, last: Date.now() }); + + const getWsUrl = () => { + const token = localStorage.getItem('token'); + const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; + return `${proto}://${window.location.host}/ws?type=rdp&agentId=${agentId}&token=${token}`; + }; + + const connect = (screen = screenIdx) => { + setStatus('connecting'); + setError(''); + const ws = new WebSocket(getWsUrl()); + wsRef.current = ws; + + ws.onopen = () => { + ws.send(JSON.stringify({ type: 'rdp_start', screen })); + setStatus('connected'); + }; + + ws.onmessage = (e) => { + try { + const msg = JSON.parse(e.data); + if (msg.type === 'rdp_frame') { + const img = new window.Image(); + img.onload = () => { + const canvas = canvasRef.current; + if (!canvas) return; + if (canvas.width !== img.naturalWidth) canvas.width = img.naturalWidth; + if (canvas.height !== img.naturalHeight) canvas.height = img.naturalHeight; + canvas.getContext('2d').drawImage(img, 0, 0); + if (msg.w && msg.h) setResolution(`${msg.w}Γ—${msg.h}`); + const now = Date.now(); + fpsRef.current.count++; + if (now - fpsRef.current.last >= 1000) { + setFps(fpsRef.current.count); + fpsRef.current = { count: 0, last: now }; + } + }; + img.src = 'data:image/jpeg;base64,' + msg.data; + } else if (msg.type === 'rdp_disconnected') { + setStatus('idle'); + setError('Agent hat die Verbindung getrennt'); + } + } catch { } + }; + + ws.onerror = () => { setStatus('error'); setError('WebSocket-Fehler'); }; + ws.onclose = () => setStatus(s => s === 'connected' ? 'idle' : s); + }; + + const disconnect = () => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ type: 'rdp_stop' })); + wsRef.current.close(); + } + wsRef.current = null; + const canvas = canvasRef.current; + if (canvas) canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); + setStatus('idle'); + setFps(0); + setResolution(''); + }; + + useEffect(() => { + if (autoConnect) connect(screenIdx); + return () => { wsRef.current?.close(); }; + }, []); + + const statusColor = { connected: '#34d399', connecting: '#f59e0b', error: '#ef4444', idle: '#6b7280' }[status]; + const statusLabel = { connected: 'Verbunden', connecting: 'Verbinde…', error: 'Fehler', idle: 'Getrennt' }[status]; + + const containerStyle = fullscreen + ? { display: 'flex', flexDirection: 'column', height: '100vh', background: '#0d1117' } + : { marginTop: 24, borderRadius: 16, border: '1px solid var(--border-color)', overflow: 'hidden', background: 'var(--bg-secondary)' }; + + return ( +
+ {/* Header */} +
+ πŸ–₯️ + Remote Desktop + {agentHostname && {agentHostname}} + + + {statusLabel} + + {status === 'connected' && resolution && ( + + {resolution} Β· {fps} fps + + )} + + {/* Screen-Selektor */} + + +
+ {!fullscreen && ( + + )} + {status === 'idle' || status === 'error' ? ( + + ) : ( + + )} +
+
+ + {/* Canvas-Bereich */} + {status === 'idle' || status === 'error' ? ( +
+ πŸ–₯️ +
+
Remote Desktop
+
BildschirmΓΌbertragung via WebSocket Β· Agent v2.2.0+
+ {error &&
{error}
} +
+
+ ) : ( +
+ + {status === 'connecting' && ( +
+ ⏳ Verbinde… +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/AgentDetailPage.jsx b/frontend/src/pages/AgentDetailPage.jsx index b9fff05..f6062b5 100644 --- a/frontend/src/pages/AgentDetailPage.jsx +++ b/frontend/src/pages/AgentDetailPage.jsx @@ -5,6 +5,7 @@ import assetService from '../services/assetService'; import api from '../services/api'; import { toast } from 'react-toastify'; import { useAuth } from '../context/AuthContext'; +import RemoteDesktopPanel from '../components/common/RemoteDesktopPanel'; // ─── CSS Variables injected inline (design from Device Detail.html) ───────────── @@ -289,136 +290,11 @@ function RemoteShell({ agentId, agentHostname }) { ); } -// ─── Remote Desktop (JPEG-over-WebSocket) ──────────────────────────────────────── +// ─── Remote Desktop (via RemoteDesktopPanel) ───────────────────────────────────── function RemoteDesktop({ agentId, agentHostname }) { - const [status, setStatus] = useState('idle'); // idle | connecting | connected | error - const [error, setError] = useState(''); - const [fps, setFps] = useState(0); - const [resolution, setResolution] = useState(''); - const canvasRef = useRef(null); - const wsRef = useRef(null); - const fpsCounterRef = useRef({ count: 0, last: Date.now() }); - - const getWsUrl = () => { - const token = localStorage.getItem('token'); - const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; - return `${proto}://${window.location.host}/ws?type=rdp&agentId=${agentId}&token=${token}`; - }; - - const connect = () => { - setStatus('connecting'); - setError(''); - - const ws = new WebSocket(getWsUrl()); - wsRef.current = ws; - - ws.onopen = () => { - ws.send(JSON.stringify({ type: 'rdp_start' })); - setStatus('connected'); - }; - - ws.onmessage = (e) => { - try { - const msg = JSON.parse(e.data); - if (msg.type === 'rdp_frame') { - const img = new window.Image(); - img.onload = () => { - const canvas = canvasRef.current; - if (!canvas) return; - if (canvas.width !== img.naturalWidth) canvas.width = img.naturalWidth; - if (canvas.height !== img.naturalHeight) canvas.height = img.naturalHeight; - canvas.getContext('2d').drawImage(img, 0, 0); - if (msg.w && msg.h) setResolution(`${msg.w}Γ—${msg.h}`); - // FPS-Counter - const now = Date.now(); - fpsCounterRef.current.count++; - if (now - fpsCounterRef.current.last >= 1000) { - setFps(fpsCounterRef.current.count); - fpsCounterRef.current = { count: 0, last: now }; - } - }; - img.src = 'data:image/jpeg;base64,' + msg.data; - } else if (msg.type === 'rdp_disconnected') { - setStatus('idle'); - setError('Agent hat die Verbindung getrennt'); - } - } catch { /* ignorieren */ } - }; - - ws.onerror = () => { setStatus('error'); setError('WebSocket-Fehler'); }; - ws.onclose = () => { setStatus('idle'); }; - }; - - const disconnect = () => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: 'rdp_stop' })); - wsRef.current.close(); - } - wsRef.current = null; - const canvas = canvasRef.current; - if (canvas) canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); - setStatus('idle'); - setFps(0); - setResolution(''); - }; - - useEffect(() => () => { wsRef.current?.close(); }, []); - - const statusColor = status === 'connected' ? '#34d399' : status === 'connecting' ? '#f59e0b' : status === 'error' ? '#ef4444' : '#6b7280'; - const statusLabel = { idle: 'Getrennt', connecting: 'Verbinde…', connected: 'Verbunden', error: 'Fehler' }[status]; - return ( -
-
- πŸ–₯️ - Remote Desktop - {agentHostname} - - - {statusLabel} - - {status === 'connected' && resolution && ( - - {resolution} Β· {fps} fps - - )} -
- {status === 'idle' || status === 'error' ? ( - - ) : ( - - )} -
-
- - {status === 'idle' || status === 'error' ? ( -
- πŸ–₯️ -
-
Remote Desktop (Beta)
-
BildschirmΓΌbertragung via WebSocket β€” Agent muss v2.2.0+ haben
- {error &&
{error}
} -
-
- ) : ( -
- - {status === 'connecting' && ( -
- Verbinde… -
- )} -
- )} -
+ ); } diff --git a/frontend/src/pages/RdpPage.jsx b/frontend/src/pages/RdpPage.jsx new file mode 100644 index 0000000..fc8a323 --- /dev/null +++ b/frontend/src/pages/RdpPage.jsx @@ -0,0 +1,28 @@ +import React, { useEffect } from 'react'; +import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import RemoteDesktopPanel from '../components/common/RemoteDesktopPanel'; + +export default function RdpPage() { + const { agentId } = useParams(); + const [searchParams] = useSearchParams(); + const { isAuthenticated } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + if (!isAuthenticated) navigate('/login', { replace: true }); + }, [isAuthenticated]); + + if (!isAuthenticated) return null; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/routes.jsx b/frontend/src/routes.jsx index c9c8620..ce1398c 100644 --- a/frontend/src/routes.jsx +++ b/frontend/src/routes.jsx @@ -50,6 +50,7 @@ import ScannerPage from './pages/ScannerPage'; import TVDashboardPage from './pages/TVDashboardPage'; import UserManagementPage from './pages/UserManagementPage'; import FeedbackPage from './pages/FeedbackPage'; +import RdpPage from './pages/RdpPage'; const L = ({ children, roles }) => ( @@ -65,6 +66,8 @@ const AppRoutes = () => { {/* TV Dashboard – kein Layout, fullscreen */} } /> + {/* Remote Desktop – kein Layout, fullscreen, eigener Tab */} + } /> {/* Public routes */} } />