Fix Session 0 issue: spawn screen capture helper in user session

- CaptureModeRunner: captures desktop in user session via schtasks,
  sends JPEG frames over named pipe to service
- RtcService: creates named pipe server, spawns helper via schtasks
  (same pattern as NotificationService), reads frames and forwards to WS
- Program.cs: added --rdp-capture <pipeName> mode
- Multi-monitor: captures all screens combined

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:49:26 +02:00
parent 1461f0fd0d
commit 45f4c7846e
3 changed files with 178 additions and 29 deletions

View File

@@ -0,0 +1,62 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO.Pipes;
namespace ITNexusAgent;
// Läuft als User-Prozess (via schtasks), nicht als SYSTEM-Service
// Captured den Desktop und schickt JPEG-Frames via Named Pipe an den Service
public static class CaptureModeRunner
{
private static readonly ImageCodecInfo JpegCodec =
ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
public static void Run(string pipeName)
{
if (string.IsNullOrEmpty(pipeName)) return;
try
{
using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.Out);
pipe.Connect(5000); // 5s timeout
var encParams = new EncoderParameters(1);
encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
// Alle Screens zusammenführen (Multi-Monitor-Support)
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;
while (pipe.IsConnected)
{
try
{
using var bmp = new Bitmap(totalW, totalH, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bmp))
g.CopyFromScreen(left, top, 0, 0, new Size(totalW, totalH));
byte[] jpeg;
using (var ms = new MemoryStream())
{
bmp.Save(ms, JpegCodec, encParams);
jpeg = ms.ToArray();
}
// Länge (4 Bytes LE) + JPEG-Daten
pipe.Write(BitConverter.GetBytes(jpeg.Length));
pipe.Write(jpeg);
pipe.Flush();
}
catch { break; }
Thread.Sleep(150); // ~6-7 fps
}
}
catch { /* Pipe nicht verfügbar oder Timeout → Exit */ }
}
}

View File

@@ -19,6 +19,10 @@ internal class Program
RunNotification(args.Length > 1 ? args[1] : "");
return;
case "--rdp-capture":
CaptureModeRunner.Run(args.Length > 1 ? args[1] : "");
return;
case "--dashboard":
RunDashboard();
return;

View File

@@ -1,5 +1,5 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;
using System.IO.Pipes;
using System.Net.WebSockets;
using System.Text;
using Newtonsoft.Json;
@@ -7,16 +7,14 @@ using Newtonsoft.Json.Linq;
namespace ITNexusAgent.Services;
// Überträgt den Bildschirm via JPEG-Frames über WebSocket an den Browser
// WebSocket-Brücke: empfängt rdp_start vom Browser, spawnt Screen-Capture-Helper
// in der User-Session (via schtasks), leitet JPEG-Frames als rdp_frame weiter
public class RtcService
{
private readonly string _serverUrl;
private readonly string _agentKey;
private readonly string _hostname;
private static readonly ImageCodecInfo JpegCodec =
ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
public RtcService(string serverUrl, string agentKey, string hostname)
{
_serverUrl = serverUrl;
@@ -67,7 +65,7 @@ public class RtcService
if (type == "rdp_start" && captureCts == null)
{
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_ = CaptureLoopAsync(ws, captureCts.Token);
_ = CapturePipeLoopAsync(ws, captureCts.Token);
AgentWorker.Log("RDP: Screen-Capture gestartet");
}
else if (type == "rdp_stop" && captureCts != null)
@@ -82,46 +80,131 @@ public class RtcService
AgentWorker.Log("RDP: Getrennt");
}
private static async Task CaptureLoopAsync(ClientWebSocket ws, CancellationToken ct)
private async Task CapturePipeLoopAsync(ClientWebSocket ws, CancellationToken ct)
{
var encParams = new EncoderParameters(1);
encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
var pipeName = $"it-nexus-rdp-{Guid.NewGuid():N}";
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
using var pipeServer = new NamedPipeServerStream(
pipeName, PipeDirection.In, 1,
PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
// Helper via schtasks in User-Session starten
if (!SpawnCaptureHelper(exePath, pipeName))
{
AgentWorker.Log("RDP: Helper-Start fehlgeschlagen");
return;
}
try
{
using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, connectCts.Token);
await pipeServer.WaitForConnectionAsync(linked.Token);
}
catch
{
AgentWorker.Log("RDP: Helper hat sich nicht verbunden");
return;
}
AgentWorker.Log("RDP: Helper verbunden, sende Frames...");
var lenBuf = new byte[4];
while (!ct.IsCancellationRequested && pipeServer.IsConnected && ws.State == WebSocketState.Open)
{
try
{
var screen = System.Windows.Forms.Screen.PrimaryScreen;
if (screen == null) { await Task.Delay(500, ct).ContinueWith(_ => { }); continue; }
var bounds = screen.Bounds;
using var bmp = new Bitmap(bounds.Width, bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bmp))
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);
byte[] jpeg;
using (var ms = new MemoryStream())
// 4-Byte Länge lesen
var read = 0;
while (read < 4)
{
bmp.Save(ms, JpegCodec, encParams);
jpeg = ms.ToArray();
var n = await pipeServer.ReadAsync(lenBuf.AsMemory(read, 4 - read), ct);
if (n == 0) goto done;
read += n;
}
var jpegLen = BitConverter.ToInt32(lenBuf);
if (jpegLen <= 0 || jpegLen > 5_000_000) continue;
// JPEG-Daten lesen
var jpeg = new byte[jpegLen];
read = 0;
while (read < jpegLen)
{
var n = await pipeServer.ReadAsync(jpeg.AsMemory(read, jpegLen - read), ct);
if (n == 0) goto done;
read += n;
}
var payload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new
{
type = "rdp_frame",
data = Convert.ToBase64String(jpeg),
w = bounds.Width,
h = bounds.Height
data = Convert.ToBase64String(jpeg)
}));
if (ws.State == WebSocketState.Open)
await ws.SendAsync(new ArraySegment<byte>(payload), WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (OperationCanceledException) { break; }
catch (Exception ex) { AgentWorker.Log($"RDP: Capture-Fehler: {ex.Message}"); }
catch (Exception ex) { AgentWorker.Log($"RDP: Pipe-Fehler: {ex.Message}"); break; }
}
// ~8 fps
await Task.Delay(125, ct).ContinueWith(_ => { });
done:
AgentWorker.Log("RDP: Frame-Loop beendet");
}
private static bool SpawnCaptureHelper(string exePath, string pipeName)
{
try
{
var fullUser = NotificationService.GetLoggedOnUser();
if (string.IsNullOrEmpty(fullUser))
{
AgentWorker.Log("RDP: Kein eingeloggter User gefunden");
return false;
}
var taskName = $"ITNexus-RDP-{pipeName[^8..]}";
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase)
? "/ru \"INTERACTIVE\""
: $"/ru \"{fullUser}\"";
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {pipeName}\" " +
$"/sc ONCE /st {triggerTime} {ruArg} /it /f";
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
p?.WaitForExit();
if (p?.ExitCode != 0)
{
AgentWorker.Log($"RDP: schtasks create fehlgeschlagen (ExitCode={p?.ExitCode})");
return false;
}
Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
{ CreateNoWindow = true })?.WaitForExit();
// Aufräumen nach kurzer Zeit
_ = Task.Run(async () =>
{
await Task.Delay(5000);
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
});
AgentWorker.Log($"RDP: Helper gestartet als '{fullUser}' (Task: {taskName})");
return true;
}
catch (Exception ex)
{
AgentWorker.Log($"RDP: SpawnHelper Fehler: {ex.Message}");
return false;
}
}
}