winipc-ua/Services/UserAgentService.cs
2025-08-03 13:58:46 +03:00

324 lines
14 KiB
C#

/* 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<UserAgentService> _logger;
private readonly WindowScanner _windowScanner;
private readonly InputHandler _inputHandler;
private readonly IPCServerService _ipcServer;
private CancellationTokenSource? _cTok;
private Task? _task;
public UserAgentService(ILogger<UserAgentService> 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<Response> 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<WindowsRequest>(request));
case CommandType.GetScreenshot:
_logger.LogInformation($"Handling GetScreenshot request (ID: {request.Id}).");
return await HandleGetScreenshotRequestAsync(request.Id, PayloadHandler.ExtractPayload<ScreenshotRequest>(request));
case CommandType.GetPixelColor:
_logger.LogInformation($"Handling GetPixelColor request (ID: {request.Id}).");
return await HandleGetPixelColorRequestAsync(request.Id, PayloadHandler.ExtractPayload<PixelRequest>(request));
case CommandType.InputAction:
_logger.LogInformation($"Handling InputAction request (ID: {request.Id}).");
return await HandleInputActionRequestAsync(request.Id, PayloadHandler.ExtractPayload<InputRequest>(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<Response> 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<Response> 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<Response> 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<T>(InputAction action) where T : IMessage<T>, new()
{
return PayloadHandler.Deserialize<T>(action.Payload.ToByteArray());
}
private async Task<Response> 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<ButtonInput>(action).Button, (int)action.DelayMs))
++nSuccess;
break;
case InputType.KeyDown:
if (await _inputHandler.KeyDown(hwnd, GetInputAction<ButtonInput>(action).Button, (int)action.DelayMs))
++nSuccess;
break;
case InputType.MouseScroll:
if (await _inputHandler.MouseScroll(hwnd, GetInputAction<ScrollInput>(action).Offset, (int)action.DelayMs))
++nSuccess;
break;
case InputType.MouseMoveTo:
var input = GetInputAction<MouseMoveInput>(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