/* This software is licensed by the MIT License, see LICENSE file */ /* Copyright © 2024-2025 Gregory Lirent */ #if WINIPC_UA_SERVER using Google.Protobuf; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using WinIPC.Models; using WinIPC.Utils; namespace WinIPC.Services { public class UserAgentService : IHostedService, IDisposable { private readonly ILogger _logger; private readonly WindowScanner _windowScanner; private readonly InputHandler _inputHandler; private readonly IPCServerService _ipcServer; private CancellationTokenSource? _cTok; private Task? _task; public UserAgentService(ILogger logger, WindowScanner windowScanner, InputHandler inputHandler, IPCServerService ipcServer) { _logger = logger; _windowScanner = windowScanner; _ipcServer = ipcServer; _inputHandler = inputHandler; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("UserAgentService is starting."); _cTok = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _task = _ipcServer.StartListening(HandleRequestAsync, _cTok.Token); return Task.CompletedTask; } public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("UserAgentService is stopping."); _cTok?.Cancel(); if (_task != null) { await _task; } _logger.LogInformation("UserAgentService stopped."); } [DllImport("kernel32.dll")] private static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ClientToScreen(IntPtr hWnd, ref RECT lpPoint); [StructLayout(LayoutKind.Explicit)] private struct RECT { [FieldOffset(0)] public int Left; [FieldOffset(4)] public int Top; [FieldOffset(8)] public int Right; [FieldOffset(12)] public int Bottom; [FieldOffset(0)] public int X; [FieldOffset(4)] public int Y; } [DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); private async Task HandleRequestAsync(Request request) { try { switch (request.Type) { case CommandType.GetWindowsInfo: _logger.LogInformation($"Handling GetWindows request (ID: {request.Id})."); return await HandleGetWindowsRequestAsync(request.Id, PayloadHandler.ExtractPayload(request)); case CommandType.GetScreenshot: _logger.LogInformation($"Handling GetScreenshot request (ID: {request.Id})."); return await HandleGetScreenshotRequestAsync(request.Id, PayloadHandler.ExtractPayload(request)); case CommandType.GetPixelColor: _logger.LogInformation($"Handling GetPixelColor request (ID: {request.Id})."); return await HandleGetPixelColorRequestAsync(request.Id, PayloadHandler.ExtractPayload(request)); case CommandType.InputAction: _logger.LogInformation($"Handling InputAction request (ID: {request.Id})."); return await HandleInputActionRequestAsync(request.Id, PayloadHandler.ExtractPayload(request)); default: _logger.LogWarning($"Unknown command type received: {request.Type} (ID: {request.Id})."); return PayloadHandler.CreateErrorResponse(request.Id, $"Unknown command type: {request.Type}"); } } catch (Exception ex) { _logger.LogError(ex, $"Error handling request ID {request.Id}, Type {request.Type}."); return PayloadHandler.CreateErrorResponse(request.Id, $"Server error: {ex.Message}"); } } private async Task HandleGetWindowsRequestAsync(uint requestId, WindowsRequest req) { try { var resp = new WindowsResponse(); foreach (var info in await _windowScanner.ScanAsync()) { if (req.Hwnds.Count == 0 || req.Hwnds.Contains(info.Hwnd)) resp.Data.Add(info); } return PayloadHandler.CreateResponse(requestId, true, null, resp); } catch (Exception ex) { _logger.LogError(ex, "Error handling GetWindows request."); return PayloadHandler.CreateErrorResponse(requestId, $"Error scanning windows: {ex.Message}"); } } private Rectangle GetCaptureArea(IntPtr hwnd, Models.Point pt, Models.AreaSize size) { Rectangle area; if (hwnd == IntPtr.Zero) { area = Rectangle.FromLTRB(GetSystemMetrics(76), GetSystemMetrics(77), GetSystemMetrics(78), GetSystemMetrics(79)); _logger.LogDebug($"Capturing entire virtual desktop screenshot. Initial source: {area}"); } else { _logger.LogDebug($"Capturing screenshot for window HWND: {hwnd} (client area)."); if (GetClientRect(hwnd, out RECT rect)) { RECT br = new RECT { X = rect.Right, Y = rect.Bottom }; if (ClientToScreen(hwnd, ref rect) && ClientToScreen(hwnd, ref br)) { area = new Rectangle(rect.X, rect.Y, br.X - rect.X, br.Y - rect.Y); _logger.LogDebug($"Window client area in screen coordinates: {area}"); } else { _logger.LogWarning($"Could not convert client coordinates to screen coordinates for HWND: {hwnd}. Returning empty byte array."); return Rectangle.Empty; } } else { _logger.LogWarning($"Failed to get client rectangle for HWND: {hwnd}. Returning empty byte array."); return Rectangle.Empty; } } if (size.Width > 0 && size.Height > 0) { _logger.LogDebug($"Applying additional crop: X={pt.X}, Y={pt.Y}, Width={size.Width}, Height={size.Height} relative to source."); area = new Rectangle( Math.Max(area.X, area.X + pt.X), Math.Max(area.Y, area.Y + pt.Y), Math.Min(size.Width, area.Right - pt.X), Math.Min(size.Height, area.Bottom - pt.Y)); _logger.LogDebug($"Final calculated capture rectangle in screen coordinates: {area}"); } else { _logger.LogInformation("No additional crop requested. Using full source area as final capture rectangle."); } if (area.Width <= 0 || area.Height <= 0) { _logger.LogWarning($"Final capture rectangle has invalid dimensions: {area}. Returning empty byte array."); return Rectangle.Empty; } return area; } private async Task HandleGetScreenshotRequestAsync(uint requestId, ScreenshotRequest req) { return await Task.Run(() => { try { Rectangle area = GetCaptureArea(req.Hwnd, req.CropPosition, req.CropSize); if (area.IsEmpty) { return PayloadHandler.CreateErrorResponse(requestId, $"Error capturing screenshot: Size cannot be calculated"); } using (Bitmap screenshot = new Bitmap(area.Width, area.Height, PixelFormat.Format32bppArgb)) { using (Graphics g = Graphics.FromImage(screenshot)) { g.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy); } _logger.LogDebug($"Successfully captured final screenshot of size: {screenshot.Width}x{screenshot.Height}"); using (MemoryStream ms = new MemoryStream()) { screenshot.Save(ms, ImageFormat.Png); return PayloadHandler.CreateResponse(requestId, true, null, new ScreenshotResponse { Size = new AreaSize { Width = screenshot.Width, Height = screenshot.Height }, Data = ByteString.CopyFrom(ms.ToArray()) }); } } } catch (Exception ex) { _logger.LogError(ex, $"Error handling GetScreenshotRequest. Returning empty byte array."); return PayloadHandler.CreateErrorResponse(requestId, $"Error capturing screenshot: {ex.Message}"); } }); } private async Task HandleGetPixelColorRequestAsync(uint requestId, PixelRequest req) { return await Task.Run(() => { try { Rectangle area = GetCaptureArea(req.Hwnd, req.PixelPosition, new AreaSize { Width = 1, Height = 1 }); if (area.IsEmpty) { return PayloadHandler.CreateErrorResponse(requestId, $"Error getting pixel color: Size cannot be calculated"); } using (Bitmap screenPixel = new Bitmap(1, 1)) { using (Graphics g = Graphics.FromImage(screenPixel)) { g.CopyFromScreen(area.Left, area.Top, 0, 0, screenPixel.Size); } Color color = screenPixel.GetPixel(0, 0); return PayloadHandler.CreateResponse(requestId, true, null, new PixelResponse { RgbColor = (uint)((color.R << 16) | (color.G << 8) | color.B) }); } } catch (Exception ex) { _logger.LogError(ex, $"Error handling GetPixelColor request for HWND {req.Hwnd}."); return PayloadHandler.CreateErrorResponse(requestId, $"Error getting pixel color: {ex.Message}"); } }); } private static T GetInputAction(InputAction action) where T : IMessage, new() { return PayloadHandler.Deserialize(action.Payload.ToByteArray()); } private async Task HandleInputActionRequestAsync(uint requestId, InputRequest req) { var hwnd = (IntPtr)req.Hwnd; int nSuccess = 0; foreach (var action in req.Actions) { try { switch (action.Type) { case InputType.KeyUp: if (await _inputHandler.KeyUp(hwnd, GetInputAction(action).Button, (int)action.DelayMs)) ++nSuccess; break; case InputType.KeyDown: if (await _inputHandler.KeyDown(hwnd, GetInputAction(action).Button, (int)action.DelayMs)) ++nSuccess; break; case InputType.MouseScroll: if (await _inputHandler.MouseScroll(hwnd, GetInputAction(action).Offset, (int)action.DelayMs)) ++nSuccess; break; case InputType.MouseMoveTo: var input = GetInputAction(action); if (await _inputHandler.MouseMove(hwnd, input.Position.X, input.Position.Y, (int)action.DelayMs)) ++nSuccess; break; default: _logger.LogWarning($"Unexpected action"); break; } } catch (Exception ex) { _logger.LogWarning(ex, $"Handling action {action.Type} was failed"); } } return PayloadHandler.CreateResponse(requestId, true, null, new InputResponse { Count = nSuccess }); } public void Dispose() { _ipcServer.Dispose(); _logger.LogInformation("UserAgentService disposed."); } } } #endif