This commit is contained in:
Gregory Lirent 2025-08-02 20:18:00 +03:00
parent 1e8a0d8756
commit 191678c8b2
12 changed files with 1548 additions and 6 deletions

18
Config/AppConfig.cs Normal file
View File

@ -0,0 +1,18 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
namespace WinIPC.Config
{
public class IPCServiceOptions
{
public string BasePipeName { get; set; } = "Global\\ProcessMonitoringService.UserAgent.Pipe";
}
#if WINIPC_UA_SERVER
public class AppConfig
{
public IPCServiceOptions IPCService { get; set; } = new IPCServiceOptions();
}
#endif
}

8
LICENSE Normal file
View File

@ -0,0 +1,8 @@
MIT License
Copyright (c) <2024-2025> <Gregory Lirent>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

245
Models/ipc.proto Normal file
View File

@ -0,0 +1,245 @@
syntax = "proto3";
option csharp_namespace = "WinIPC.Models";
enum CommandType {
UNKNOWN_COMMAND_TYPE = 0;
GET_WINDOWS_INFO = 1;
GET_SCREENSHOT = 2;
GET_PIXEL_COLOR = 3;
INPUT_ACTION = 4;
}
message Point {
int32 X = 1;
int32 Y = 2;
}
message AreaSize {
int32 width = 1;
int32 height = 2;
}
message WindowInfo
{
int32 hwnd = 1;
string title = 2;
int32 pid = 3;
int32 thread_id = 4;
bool is_active = 5;
Point cursor_position = 6;
AreaSize content_size = 7;
Point window_position = 8;
AreaSize window_size = 9;
}
message WindowsRequest {
repeated int32 hwnds = 1;
}
message WindowsResponse {
repeated WindowInfo data = 1;
}
message Request {
uint32 id = 1;
CommandType type = 2;
bytes payload = 15;
}
message Response {
uint32 id = 1;
bool success = 2;
string message = 3;
bytes payload = 15;
}
message ScreenshotRequest {
int32 hwnd = 1;
Point crop_position = 2;
AreaSize crop_size = 3;
}
message ScreenshotResponse {
AreaSize size = 1;
bytes data = 2;
}
message PixelRequest {
int32 hwnd = 1;
Point pixel_position = 2;
}
message PixelResponse {
uint32 rgb_color = 1;
}
enum InputType {
UNKNOWN_INPUT_TYPE = 0;
KEY_UP = 1;
KEY_DOWN = 2;
MOUSE_SCROLL = 3;
MOUSE_MOVE_TO = 4;
}
message InputAction {
InputType type = 1;
uint32 delay_ms = 2;
bytes payload = 15;
}
message ButtonInput {
Button button = 1;
}
message ScrollInput {
int32 offset = 1;
}
message MouseMoveInput {
Point position = 1;
}
message InputRequest {
int32 hwnd = 1;
repeated InputAction actions = 2;
}
message InputResponse {
int32 count = 1;
}
enum Button {
UNKNOWN_BUTTON = 0;
EXTENDED_KEY_FLAG = 32768; // 0x8000
MOUSE_EXTRA_FLAG = 128; // 0x0080
MOUSE_KEY_FLAG = 16384; // 0x4000
MOUSE_LEFT = 16386; // MOUSE_KEY_FLAG | 0x0002
MOUSE_RIGHT = 16392; // MOUSE_KEY_FLAG | 0x0008
MOUSE_MIDDLE = 16416; // MOUSE_KEY_FLAG | 0x0020
MOUSE_EXTRA1 = 16513; // MOUSE_KEY_FLAG | MOUSE_EXTRA_FLAG | 0x01
MOUSE_EXTRA2 = 16514; // MOUSE_KEY_FLAG | MOUSE_EXTRA_FLAG | 0x02
// --- Toggle Keys ---
CAPS_LOCK = 20; // 0x14
NUM_LOCK = 144; // 0x90
SCROLL_LOCK = 145; // 0x91
// --- Main Control Keys ---
BACKSPACE = 8; // 0x08
TAB = 9; // 0x09
ENTER = 13; // 0x0D
PAUSE = 19; // 0x13
ESCAPE = 27; // 0x1B
SPACE = 32; // 0x20
// --- Navigation and Editing Keys ---
PAGE_UP = 32793; // EXTENDED_KEY_FLAG | 0x21
PAGE_DOWN = 32794; // EXTENDED_KEY_FLAG | 0x22
END = 32795; // EXTENDED_KEY_FLAG | 0x23
HOME = 32796; // EXTENDED_KEY_FLAG | 0x24
LEFT = 32797; // EXTENDED_KEY_FLAG | 0x25
UP = 32798; // EXTENDED_KEY_FLAG | 0x26
RIGHT = 32799; // EXTENDED_KEY_FLAG | 0x27
DOWN = 32800; // EXTENDED_KEY_FLAG | 0x28
PRINT_SCREEN = 32808; // EXTENDED_KEY_FLAG | 0x2C
INSERT = 32813; // EXTENDED_KEY_FLAG | 0x2D
DELETE = 32814; // EXTENDED_KEY_FLAG | 0x2E
// --- Main Alphanumeric Keys ---
KEY_0 = 48; // 0x30
KEY_1 = 49; // 0x31
KEY_2 = 50; // 0x32
KEY_3 = 51; // 0x33
KEY_4 = 52; // 0x34
KEY_5 = 53; // 0x35
KEY_6 = 54; // 0x36
KEY_7 = 55; // 0x37
KEY_8 = 56; // 0x38
KEY_9 = 57; // 0x39
KEY_A = 65; // 0x41
KEY_B = 66; // 0x42
KEY_C = 67; // 0x43
KEY_D = 68; // 0x44
KEY_E = 69; // 0x45
KEY_F = 70; // 0x46
KEY_G = 71; // 0x47
KEY_H = 72; // 0x48
KEY_I = 73; // 0x49
KEY_J = 74; // 0x4A
KEY_K = 75; // 0x4B
KEY_L = 76; // 0x4C
KEY_M = 77; // 0x4D
KEY_N = 78; // 0x4E
KEY_O = 79; // 0x4F
KEY_P = 80; // 0x50
KEY_Q = 81; // 0x51
KEY_R = 82; // 0x52
KEY_S = 83; // 0x53
KEY_T = 84; // 0x54
KEY_U = 85; // 0x55
KEY_V = 86; // 0x56
KEY_W = 87; // 0x57
KEY_X = 88; // 0x58
KEY_Y = 89; // 0x59
KEY_Z = 90; // 0x5A
// --- Windows Keys ---
L_WIN = 32859; // EXTENDED_KEY_FLAG | 0x5B
R_WIN = 32860; // EXTENDED_KEY_FLAG | 0x5C
APPS = 32861; // EXTENDED_KEY_FLAG | 0x5D
// --- Numeric Keypad (NumPad) ---
NUM_0 = 32864; // EXTENDED_KEY_FLAG | 0x60
NUM_1 = 32865; // EXTENDED_KEY_FLAG | 0x61
NUM_2 = 32866; // EXTENDED_KEY_FLAG | 0x62
NUM_3 = 32867; // EXTENDED_KEY_FLAG | 0x63
NUM_4 = 32868; // EXTENDED_KEY_FLAG | 0x64
NUM_5 = 32869; // EXTENDED_KEY_FLAG | 0x65
NUM_6 = 32870; // EXTENDED_KEY_FLAG | 0x66
NUM_7 = 32871; // EXTENDED_KEY_FLAG | 0x67
NUM_8 = 32872; // EXTENDED_KEY_FLAG | 0x68
NUM_9 = 32873; // EXTENDED_KEY_FLAG | 0x69
NUM_MUL = 106; // 0x6A
NUM_ADD = 107; // 0x6B
NUM_SUB = 109; // 0x6D
NUM_DEC = 32878; // EXTENDED_KEY_FLAG | 0x6E
NUM_DIV = 32879; // EXTENDED_KEY_FLAG | 0x6F
NUM_ENTER = 32781; // EXTENDED_KEY_FLAG | ENTER
// --- Function Keys ---
F1 = 112; // 0x70
F2 = 113; // 0x71
F3 = 114; // 0x72
F4 = 115; // 0x73
F5 = 116; // 0x74
F6 = 117; // 0x75
F7 = 118; // 0x76
F8 = 119; // 0x77
F9 = 120; // 0x78
F10 = 121; // 0x79
F11 = 122; // 0x7A
F12 = 123; // 0x7B
// --- Modifier Keys ---
L_SHIFT = 160; // 0xA0
R_SHIFT = 161; // 0xA1
L_CTRL = 162; // 0xA2
R_CTRL = 32875; // EXTENDED_KEY_FLAG | 0xA3
L_ALT = 164; // 0xA4
R_ALT = 32877; // EXTENDED_KEY_FLAG | 0xA5
// --- OEM Keys ---
SUB = 189; // 0xBD
ADD = 187; // 0xBB
TILDE = 192; // 0xC0
L_BRACKET = 219; // 0xDB
R_BRACKET = 221; // 0xDD
COMMA = 188; // 0xBC
PERIOD = 190; // 0xBE
QUOTE = 222; // 0xDE
COLON = 186; // 0xBA
SOL = 191; // 0xBF
B_SOL = 220; // 0xDC
}

46
Program.cs Normal file
View File

@ -0,0 +1,46 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
#if WINIPC_UA_SERVER
using System.Runtime.Versioning;
using WinIPC.Config;
using WinIPC.Services;
using WinIPC.Utils;
[assembly: SupportedOSPlatform("windows")]
namespace UserSessionAgent
{
public class Program
{
public static async Task Main(string[] args)
{
var hostBuilder = CreateHostBuilder(args);
var host = hostBuilder.Build();
await host.RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
})
.ConfigureServices((hostContext, services) =>
{
services.AddOptions();
services.Configure<AppConfig>(hostContext.Configuration.GetSection("AppConfig"));
services.AddSingleton<WindowScanner>();
services.AddSingleton<InputHandler>();
services.AddSingleton<IPCServerService>();
services.AddSingleton<UserAgentService>();
services.AddHostedService(sp => sp.GetRequiredService<IPCServerService>());
services.AddHostedService(sp => sp.GetRequiredService<UserAgentService>());
});
}
}
#endif

View File

@ -0,0 +1,269 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
#if WINIPC_UA_SERVER
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.IO.Pipes;
using WinIPC.Config;
using WinIPC.Models;
using WinIPC.Utils;
namespace WinIPC.Services
{
public class IPCServerService : IHostedService, IDisposable
{
private readonly ILogger<IPCServerService> _logger;
private readonly IPCServiceOptions _options;
private readonly string _pipeName;
private NamedPipeServerStream? _pipe;
private CancellationTokenSource? _cTok;
private Task? _task;
private Func<Request, Task<Response>>? _handler;
public IPCServerService(ILogger<IPCServerService> logger, IOptions<AppConfig> appConfigOptions)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = appConfigOptions?.Value?.IPCService ?? throw new ArgumentNullException(nameof(appConfigOptions));
int sid = Process.GetCurrentProcess().SessionId;
if (sid == 0)
{
_logger.LogWarning("Server: Failed to get current Session ID (returned 0). Using base pipe name without Session ID suffix.");
_pipeName = _options.BasePipeName;
}
else
{
_pipeName = $"{_options.BasePipeName}.{sid}";
_logger.LogInformation("Server: Appending Session ID {SessionId} to pipe name. Actual pipe name: {ActualPipeName}", sid, _pipeName);
}
_logger.LogInformation("IPCServerService initialized. Actual pipe name: {ActualPipeName}", _pipeName);
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IPCServerService is starting...");
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IPCServerService is stopping...");
_cTok?.Cancel();
if (_task != null)
{
try
{
await _task;
}
catch (OperationCanceledException) { /* Expected */ }
catch (Exception ex) { _logger.LogError(ex, "Error while waiting for server listening task to complete."); }
}
Dispose();
_logger.LogInformation("IPCServerService stopped.");
}
public void Dispose()
{
_cTok?.Dispose();
_pipe?.Dispose();
GC.SuppressFinalize(this);
}
public Task StartListening(Func<Request, Task<Response>> handler, CancellationToken cTok)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
_cTok = CancellationTokenSource.CreateLinkedTokenSource(cTok);
_task = Task.Run(() => ServerListenLoop(_cTok.Token), _cTok.Token);
return _task;
}
private async Task ServerListenLoop(CancellationToken cTok)
{
_logger.LogInformation($"Server: Starting listen loop on pipe '{_pipeName}'...");
while (!cTok.IsCancellationRequested)
{
try
{
_pipe = new NamedPipeServerStream(
_pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
_logger.LogInformation("Server: Waiting for client connection...");
await _pipe.WaitForConnectionAsync(cTok);
_logger.LogInformation("Server: Client connected.");
_ = HandleClientConnectionAsync(_pipe, cTok);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Server: Listen loop cancelled.");
_pipe?.Dispose();
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Server: Error in main listen loop. Disposing current pipe and continuing to next iteration.");
_pipe?.Dispose();
await Task.Delay(1000, cTok);
}
}
_logger.LogInformation("Server: Listen loop finished.");
}
private async Task HandleClientConnectionAsync(NamedPipeServerStream pipe, CancellationToken cTok)
{
try
{
_logger.LogInformation($"Server: Handling connection from pipe '{_pipeName}'...");
while (pipe.IsConnected && !cTok.IsCancellationRequested)
{
byte[]? messageBytes = null;
try
{
messageBytes = await ReadMessageAsync(pipe, cTok);
}
catch (EndOfStreamException)
{
_logger.LogInformation($"Server: Client disconnected from pipe '{_pipeName}'.");
break;
}
catch (OperationCanceledException)
{
_logger.LogInformation($"Server: Message reading cancelled for pipe '{_pipeName}'.");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Server: Error reading message from pipe '{_pipeName}'.");
break;
}
if (messageBytes == null || messageBytes.Length == 0)
{
_logger.LogDebug($"Server: Received empty or null message from pipe '{_pipeName}'. Continuing...");
continue;
}
_logger.LogDebug($"Server: Received {messageBytes.Length} bytes from pipe '{_pipeName}'.");
Response response = await ProcessIncomingRequest(messageBytes);
bool writeSuccess = await WriteMessageAsync(pipe, PayloadHandler.Serialize(response), cTok);
if (!writeSuccess)
{
_logger.LogError($"Server: Failed to write response to pipe '{_pipeName}'.");
break;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Server: Unhandled exception in client connection handler for pipe '{_pipeName}'.");
}
finally
{
_logger.LogInformation($"Server: Client connection handler finished for pipe '{_pipeName}'. Disposing pipe.");
pipe.Dispose();
}
}
private async Task<Response> ProcessIncomingRequest(byte[] messageBytes)
{
Request request;
try
{
request = PayloadHandler.Deserialize<Request>(messageBytes);
}
catch (Exception ex)
{
_logger.LogError(ex, "Server: Failed to deserialize incoming message as Request. Returning error response.");
return PayloadHandler.CreateErrorResponse(0, $"Failed to deserialize request: {ex.Message}");
}
if (request.Id > 0 && request.Type != CommandType.UnknownCommandType)
{
_logger.LogInformation("Server: Received request ID {RequestId}, Type: {CommandType}.", request.Id, request.Type);
if (_handler != null)
{
try
{
return await _handler(request);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Server: Error processing request ID {request.Id}, Type {request.Type} by handler.");
return PayloadHandler.CreateErrorResponse(request.Id, $"Handler error: {ex.Message}");
}
}
else
{
_logger.LogWarning("Server: No request handler registered for IPCServerService.");
return PayloadHandler.CreateErrorResponse(request.Id, "No handler registered on server.");
}
}
else
{
_logger.LogWarning("Server: Received invalid Protobuf request (ID or Type missing). Raw bytes: {Bytes}", BitConverter.ToString(messageBytes));
return PayloadHandler.CreateErrorResponse(request.Id, "Invalid request format.");
}
}
private async Task<byte[]> ReadMessageAsync(PipeStream pipe, CancellationToken cTok)
{
byte[] lengthBuffer = new byte[4];
int bytesRead;
bytesRead = await pipe.ReadAsync(lengthBuffer, 0, 4, cTok);
if (bytesRead == 0) throw new EndOfStreamException("Pipe closed or no data for length prefix.");
if (bytesRead < 4) throw new IOException($"Failed to read full 4-byte length prefix. Read {bytesRead} bytes.");
int messageLength = BitConverter.ToInt32(lengthBuffer, 0);
if (messageLength <= 0) throw new InvalidDataException($"Received invalid message length: {messageLength}.");
byte[] messageBuffer = new byte[messageLength];
int totalBytesRead = 0;
while (totalBytesRead < messageLength)
{
bytesRead = await pipe.ReadAsync(messageBuffer, totalBytesRead, messageLength - totalBytesRead, cTok);
if (bytesRead == 0) throw new EndOfStreamException($"Pipe closed unexpectedly while reading message payload. Expected {messageLength} bytes, read {totalBytesRead}.");
totalBytesRead += bytesRead;
}
return messageBuffer;
}
private async Task<bool> WriteMessageAsync(PipeStream pipe, byte[] buffer, CancellationToken cTok)
{
try
{
byte[] lengthPrefix = BitConverter.GetBytes(buffer.Length);
await pipe.WriteAsync(lengthPrefix, 0, lengthPrefix.Length, cTok);
await pipe.WriteAsync(buffer, 0, buffer.Length, cTok);
await pipe.FlushAsync(cTok);
return true;
}
catch (OperationCanceledException)
{
_logger.LogInformation("WriteMessageAsync: Operation cancelled.");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"WriteMessageAsync: Error writing to pipe. Buffer size: {buffer.Length} bytes.");
}
return false;
}
}
}
#endif

View File

@ -0,0 +1,351 @@
/* 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 data = new List<WindowInfo>();
foreach (var info in await _windowScanner.ScanAsync())
{
if (req.Hwnds.Count == 0 || req.Hwnds.Contains(info.Hwnd)) data.Add(info);
}
var windowsResponse = new WindowsResponse
{
Data = { data }
};
return new Response
{
Id = requestId,
Success = true,
Payload = ByteString.CopyFrom(PayloadHandler.Serialize(windowsResponse))
};
}
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 new Response
{
Id = requestId,
Success = true,
Payload = ByteString.CopyFrom(PayloadHandler.Serialize(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 new Response
{
Id = requestId,
Success = true,
Payload = ByteString.CopyFrom(PayloadHandler.Serialize(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 new Response
{
Id = requestId,
Success = true,
Payload = ByteString.CopyFrom(PayloadHandler.Serialize(new InputResponse
{
Count = nSuccess
}))
};
}
public void Dispose()
{
_ipcServer.Dispose();
_logger.LogInformation("UserAgentService disposed.");
}
}
}
#endif

332
Utils/InputHandler.cs Normal file
View File

@ -0,0 +1,332 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
#if WINIPC_UA_SERVER
using System.Runtime.InteropServices;
using WinIPC.Models;
namespace WinIPC.Utils
{
public class InputHandler
{
private Dictionary<IntPtr, Dictionary<Button, bool>> _state = new();
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, IntPtr pInputs, int cbSize);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern short GetKeyState(int nVirtKey);
[DllImport("user32.dll")]
private static extern int MapVirtualKey(int uCode, uint uMapType);
private bool IsKeyPressed(int keyCode)
{
return (GetKeyState(keyCode) & 0x8000) != 0;
}
private ushort GetMouseKeyStates()
{
ushort keyStates = 0;
if (IsKeyPressed(0x01)) keyStates |= 0x0001; // lbutton
if (IsKeyPressed(0x02)) keyStates |= 0x0002; // rbutton
if (IsKeyPressed(0x04)) keyStates |= 0x0010; // mbutton
if (IsKeyPressed(0x05)) keyStates |= 0x0020; // xbutton1
if (IsKeyPressed(0x06)) keyStates |= 0x0040; // xbutton2
if (IsKeyPressed(0x10)) keyStates |= 0x0004; // Shift
if (IsKeyPressed(0x11)) keyStates |= 0x0008; // Ctrl
return keyStates;
}
[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT
{
public int dx;
public int dy;
public int mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Explicit)]
private struct INPUT
{
[FieldOffset(0)]
public int type;
[FieldOffset(8)]
public MOUSEINPUT mi;
[FieldOffset(8)]
public KEYBDINPUT ki;
}
private static IntPtr MakeParam(int low, int high = 0)
{
return (high << 16) | (low & 0xffff);
}
private static IntPtr GetCursorPos(IntPtr hwnd)
{
WindowScanner.GetCursorPos(out var pos);
if (hwnd != IntPtr.Zero)
{
WindowScanner.ScreenToClient(hwnd, ref pos);
}
return MakeParam(pos.X, pos.Y);
}
private static readonly int INPUT_SIZE = Marshal.SizeOf(typeof(INPUT));
private Dictionary<Button, bool> GetStateDict(IntPtr hwnd)
{
Dictionary<Button, bool> state;
if (_state.TryGetValue(hwnd, out var s))
{
state = s;
}
else { _state.Add(hwnd, state = new()); }
return state;
}
private async Task<bool> ProcessMouseInput(IntPtr hwnd, Button button, bool press)
{
bool success = false;
var state = GetStateDict(hwnd);
if (state.TryGetValue(button, out var s) && s == press) return true;
return await Task.Run(async () =>
{
if (hwnd == IntPtr.Zero)
{
var buffer = Marshal.AllocHGlobal(INPUT_SIZE);
var input = new INPUT
{
type = 0x00,
mi = new()
};
if (button.HasFlag(Button.MouseExtraFlag))
{
input.mi.dwFlags = (uint)Button.MouseExtraFlag << (press ? 0 : 1);
input.mi.mouseData = (int)(button & ~Button.MouseExtraFlag);
}
else
{
input.mi.dwFlags = (uint)button << (press ? 0 : 1);
}
Marshal.StructureToPtr(input, buffer, true);
success = await Task.Run(() => { return 1 != SendInput(1, buffer, INPUT_SIZE); });
Marshal.FreeHGlobal(buffer);
}
else
{
int msg = 0;
int wFlags = 0;
switch(button)
{
case Button.MouseLeft:
msg = press ? 0x0201 : 0x0202;
break;
case Button.MouseRight:
msg = press ? 0x0204 : 0x0205;
break;
case Button.MouseMiddle:
msg = press ? 0x0207 : 0x0208;
break;
case Button.MouseExtra1:
case Button.MouseExtra2:
msg = press ? 0x020b : 0x020c;
wFlags = (int)button & 0x7f;
break;
default:
// TODO Log
break;
}
SendMessage(hwnd, msg, MakeParam(GetMouseKeyStates(), wFlags), GetCursorPos(hwnd));
success = true;
}
if (success) state[button] = press;
return success;
});
}
private async Task<bool> ProcessKeyboardInput(IntPtr hwnd, Button button, bool press)
{
bool success = false;
var state = GetStateDict(hwnd);
if (state.TryGetValue(button, out var s) && s == press) return true;
return await Task.Run<bool>(async () =>
{
if (hwnd == IntPtr.Zero)
{
var buffer = Marshal.AllocHGlobal(INPUT_SIZE);
var flags = (uint)(press ? 0x00 : 0x02);
var input = new INPUT
{
type = 0x01,
ki = new()
};
if (button.HasFlag(Button.ExtendedKeyFlag))
{
flags |= 0x01;
button &= ~Button.ExtendedKeyFlag;
}
input.ki.wVk = (ushort)button;
input.ki.dwFlags = flags;
Marshal.StructureToPtr(input, buffer, true);
success = await Task.Run(() => { return 1 != SendInput(1, buffer, INPUT_SIZE); });
Marshal.FreeHGlobal(buffer);
}
else
{
var btn = (int)(button & ~Button.ExtendedKeyFlag);
var msg = press ? 0x0100 : 0x0101;
if (button == Button.LAlt || button == Button.RAlt) msg += 4;
int flags = MapVirtualKey(btn, 0) | (button.HasFlag(Button.ExtendedKeyFlag) ? 0x0100 : 0) |
(IsKeyPressed(0x12) ? 0x2000 : 0) | (s ? 0x4000 : 0) | (press ? 0 : 0x8000);
SendMessage(hwnd, msg, MakeParam(btn), MakeParam(1, flags));
success = true;
}
if (success) state[button] = press;
return success;
});
}
public async Task<bool> KeyUp(IntPtr hwnd, Button button, int delay)
{
Task<bool> pTask;
bool success;
if (button.HasFlag(Button.MouseKeyFlag))
{
pTask = ProcessMouseInput(hwnd, button & ~Button.MouseKeyFlag, false);
}
else
{
pTask = ProcessKeyboardInput(hwnd, button, false);
}
if (success = await pTask)
{
await Task.Delay(delay);
}
return success;
}
public async Task<bool> KeyDown(IntPtr hwnd, Button button, int delay)
{
Task<bool> pTask;
bool success;
if (button.HasFlag(Button.MouseKeyFlag))
{
pTask = ProcessMouseInput(hwnd, button & ~Button.MouseKeyFlag, true);
}
else
{
pTask = ProcessKeyboardInput(hwnd, button, true);
}
if (success = await pTask)
{
await Task.Delay(delay);
}
return success;
}
public async Task<bool> MouseScroll(IntPtr hwnd, int offset, int delay)
{
bool success = false;
if (hwnd == IntPtr.Zero)
{
var buffer = Marshal.AllocHGlobal(INPUT_SIZE);
var input = new INPUT
{
type = 0x00,
mi = new MOUSEINPUT
{
dwFlags = 0x0800,
mouseData = offset
}
};
Marshal.StructureToPtr(input, buffer, true);
success = await Task.Run(() => { return 1 != SendInput(1, buffer, INPUT_SIZE); });
Marshal.FreeHGlobal(buffer);
}
else
{
SendMessage(hwnd, 0x020a, MakeParam(GetMouseKeyStates(), offset), GetCursorPos(IntPtr.Zero));
success = true;
}
await Task.Delay(delay);
return success;
}
public async Task<bool> MouseMove(IntPtr hwnd, int x, int y, int delay)
{
bool success = false;
if (hwnd == IntPtr.Zero)
{
var buffer = Marshal.AllocHGlobal(INPUT_SIZE);
var input = new INPUT
{
type = 0x00,
mi = new MOUSEINPUT
{
dwFlags = 0x8001,
dx = x,
dy = y
}
};
Marshal.StructureToPtr(input, buffer, true);
success = await Task.Run(() => { return 1 != SendInput(1, buffer, INPUT_SIZE); });
Marshal.FreeHGlobal(buffer);
}
else
{
SendMessage(hwnd, 0x0200, MakeParam(GetMouseKeyStates()), MakeParam(x, y));
success = true;
}
await Task.Delay(delay);
return success;
}
}
}
#endif

100
Utils/PayloadHandler.cs Normal file
View File

@ -0,0 +1,100 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
using Google.Protobuf;
using System.Reflection;
using WinIPC.Models;
namespace WinIPC.Utils
{
public static class PayloadHandler
{
public static byte[] Serialize<T>(T message) where T : IMessage<T>
{
if (message == null)
{
throw new ArgumentNullException(nameof(message), "Protobuf message cannot be null for serialization.");
}
return message.ToByteArray();
}
public static T Deserialize<T>(byte[] data) where T : IMessage<T>, new()
{
if (data == null)
{
throw new ArgumentNullException(nameof(data), "Byte array cannot be null for deserialization.");
}
PropertyInfo? parserProperty = typeof(T).GetProperty("Parser", BindingFlags.Static | BindingFlags.Public);
if (parserProperty == null)
{
throw new InvalidOperationException($"Type {typeof(T).Name} does not have a static 'Parser' property.");
}
MessageParser<T>? parser = parserProperty.GetValue(null) as MessageParser<T>;
if (parser == null)
{
throw new InvalidOperationException($"Could not get MessageParser for type {typeof(T).Name}.");
}
return parser.ParseFrom(data);
}
public static Request CreateRequest<T>(uint id, CommandType commandType, T payloadMessage) where T : IMessage<T>
{
return new Request
{
Id = id,
Type = commandType,
Payload = ByteString.CopyFrom(Serialize(payloadMessage))
};
}
public static Response CreateResponse<T>(uint id, bool success, string? errorMessage, T payloadMessage) where T : IMessage<T>
{
return new Response
{
Id = id,
Success = success,
Message = errorMessage ?? string.Empty,
Payload = ByteString.CopyFrom(Serialize(payloadMessage))
};
}
public static Response CreateErrorResponse(uint id, string errorMessage)
{
return new Response
{
Id = id,
Success = false,
Message = errorMessage
};
}
public static T ExtractPayload<T>(Request request) where T : IMessage<T>, new()
{
if (request == null)
{
throw new ArgumentNullException(nameof(request), "Request cannot be null.");
}
if (request.Payload == null || request.Payload.IsEmpty)
{
throw new ArgumentNullException(nameof(request.Payload), "Request payload is null or empty.");
}
return Deserialize<T>(request.Payload.ToByteArray());
}
public static T ExtractPayload<T>(Response response) where T : IMessage<T>, new()
{
if (response == null)
{
throw new ArgumentNullException(nameof(response), "Response cannot be null.");
}
if (response.Payload == null || response.Payload.IsEmpty)
{
throw new ArgumentNullException(nameof(response.Payload), "Response payload is null or empty.");
}
return Deserialize<T>(response.Payload.ToByteArray());
}
}
}

133
Utils/WindowScanner.cs Normal file
View File

@ -0,0 +1,133 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
#if WINIPC_UA_SERVER
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using WinIPC.Models;
namespace WinIPC.Utils
{
public class WindowScanner
{
[StructLayout(LayoutKind.Explicit)]
internal 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;
}
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(out RECT lpPoint);
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ScreenToClient(IntPtr hWnd, ref RECT lpPoint);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", SetLastError = true)]
public static extern long GetWindowLongPtr(IntPtr hWnd, int nIndex);
private bool GetWindowInfo(ref WindowInfo data, IntPtr hwnd)
{
int length = GetWindowTextLength(hwnd);
if (length > 0)
{
var sb = new StringBuilder(length + 1);
GetWindowText(hwnd, sb, sb.Capacity);
data.Title = sb.ToString();
if (GetCursorPos(out RECT pt) && ScreenToClient(hwnd, ref pt))
{
data.CursorPosition = new Models.Point { X = pt.X, Y = pt.Y };
}
if (GetWindowRect(hwnd, out RECT wRect))
{
var rect = Rectangle.FromLTRB(wRect.Left, wRect.Top, wRect.Right, wRect.Bottom);
data.WindowPosition = new Models.Point { X = rect.X, Y = rect.Y };
data.WindowSize = new Models.AreaSize { Width = rect.Width, Height = rect.Height };
}
if (GetClientRect(hwnd, out RECT cRect))
{
var rect = Rectangle.FromLTRB(cRect.Left, cRect.Top, cRect.Right, cRect.Bottom);
data.ContentSize = new Models.AreaSize { Width = rect.Width, Height = rect.Height };
}
return true;
}
return false;
}
public async Task<IEnumerable<WindowInfo>> ScanAsync(ushort max = 0)
{
if (max == 0) max = UInt16.MaxValue;
return await Task.Run(() =>
{
var foreground = GetForegroundWindow();
List<WindowInfo> container = new();
EnumWindows(delegate (IntPtr hwnd, IntPtr lParam)
{
if (container.Count >= max) return false;
if (IsWindowVisible(hwnd))
{
WindowInfo data = new();
data.Hwnd = hwnd.ToInt32();
if ((data.ThreadId = (int)GetWindowThreadProcessId(hwnd, out uint pid)) != 0)
{
data.Pid = (int)pid;
if (GetWindowInfo(ref data, hwnd))
{
data.IsActive = foreground.ToInt32() == data.Hwnd;
container.Add(data);
}
}
}
return true;
}, IntPtr.Zero);
return container;
});
}
}
}
#endif

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -1,14 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<UseAppHost>true</UseAppHost>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableCETCompat>true</EnableCETCompat>
<RootNamespace>WinIPC</RootNamespace>
<AssemblyName>UserAgent</AssemblyName>
@ -21,14 +18,23 @@
<Description>Deep identify processes.</Description>
<Copyright>Copyright © 2024-2025 Gregory Lirent</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<DefineConstants>WINIPC_UA_SERVER</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
<PackageReference Include="Grpc.Tools" Version="2.63.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Models\ipc.proto" GrpcServices="None" ProtoRoot="." />
</ItemGroup>
</Project>

25
winipc-ua.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36221.1 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winipc-ua", "winipc-ua.csproj", "{AFD20A99-4228-EA6F-C4DA-6FD89C9AD520}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AFD20A99-4228-EA6F-C4DA-6FD89C9AD520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AFD20A99-4228-EA6F-C4DA-6FD89C9AD520}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFD20A99-4228-EA6F-C4DA-6FD89C9AD520}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFD20A99-4228-EA6F-C4DA-6FD89C9AD520}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C136D00D-5F11-4685-8257-F334B4E20219}
EndGlobalSection
EndGlobal