324 lines
14 KiB
C#
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 |