diff --git a/Config/AppConfig.cs b/Config/AppConfig.cs new file mode 100644 index 0000000..3cebcb3 --- /dev/null +++ b/Config/AppConfig.cs @@ -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 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7449de5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +MIT License +Copyright (c) <2024-2025> + +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. \ No newline at end of file diff --git a/Models/ipc.proto b/Models/ipc.proto new file mode 100644 index 0000000..e4ef1f6 --- /dev/null +++ b/Models/ipc.proto @@ -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 +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f61e18f --- /dev/null +++ b/Program.cs @@ -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(hostContext.Configuration.GetSection("AppConfig")); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + }); + } +} +#endif \ No newline at end of file diff --git a/Services/IPCServerService.cs b/Services/IPCServerService.cs new file mode 100644 index 0000000..4a46ed9 --- /dev/null +++ b/Services/IPCServerService.cs @@ -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 _logger; + private readonly IPCServiceOptions _options; + private readonly string _pipeName; + + private NamedPipeServerStream? _pipe; + private CancellationTokenSource? _cTok; + private Task? _task; + + private Func>? _handler; + + public IPCServerService(ILogger logger, IOptions 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> 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 ProcessIncomingRequest(byte[] messageBytes) + { + Request request; + try + { + request = PayloadHandler.Deserialize(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 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 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 \ No newline at end of file diff --git a/Services/UserAgentService.cs b/Services/UserAgentService.cs new file mode 100644 index 0000000..c67a1e6 --- /dev/null +++ b/Services/UserAgentService.cs @@ -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 _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 data = new List(); + + 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 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 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(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 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 \ No newline at end of file diff --git a/Utils/InputHandler.cs b/Utils/InputHandler.cs new file mode 100644 index 0000000..2599b71 --- /dev/null +++ b/Utils/InputHandler.cs @@ -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> _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 GetStateDict(IntPtr hwnd) + { + Dictionary state; + if (_state.TryGetValue(hwnd, out var s)) + { + state = s; + } + else { _state.Add(hwnd, state = new()); } + + return state; + } + + private async Task 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 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(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 KeyUp(IntPtr hwnd, Button button, int delay) + { + Task 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 KeyDown(IntPtr hwnd, Button button, int delay) + { + Task 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 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 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 \ No newline at end of file diff --git a/Utils/PayloadHandler.cs b/Utils/PayloadHandler.cs new file mode 100644 index 0000000..4610689 --- /dev/null +++ b/Utils/PayloadHandler.cs @@ -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 message) where T : IMessage + { + if (message == null) + { + throw new ArgumentNullException(nameof(message), "Protobuf message cannot be null for serialization."); + } + return message.ToByteArray(); + } + + public static T Deserialize(byte[] data) where T : IMessage, 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? parser = parserProperty.GetValue(null) as MessageParser; + if (parser == null) + { + throw new InvalidOperationException($"Could not get MessageParser for type {typeof(T).Name}."); + } + + return parser.ParseFrom(data); + } + + public static Request CreateRequest(uint id, CommandType commandType, T payloadMessage) where T : IMessage + { + return new Request + { + Id = id, + Type = commandType, + Payload = ByteString.CopyFrom(Serialize(payloadMessage)) + }; + } + + public static Response CreateResponse(uint id, bool success, string? errorMessage, T payloadMessage) where T : IMessage + { + 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(Request request) where T : IMessage, 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(request.Payload.ToByteArray()); + } + + public static T ExtractPayload(Response response) where T : IMessage, 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(response.Payload.ToByteArray()); + } + } +} diff --git a/Utils/WindowScanner.cs b/Utils/WindowScanner.cs new file mode 100644 index 0000000..870e52d --- /dev/null +++ b/Utils/WindowScanner.cs @@ -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> ScanAsync(ushort max = 0) + { + if (max == 0) max = UInt16.MaxValue; + + return await Task.Run(() => + { + var foreground = GetForegroundWindow(); + List 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 \ No newline at end of file diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/winipc-ua.csproj b/winipc-ua.csproj index cd3574b..bf2ed4a 100644 --- a/winipc-ua.csproj +++ b/winipc-ua.csproj @@ -1,14 +1,11 @@  - net8.0 + net8.0-windows enable enable - true - true true - true WinIPC UserAgent @@ -21,14 +18,23 @@ Deep identify processes. Copyright © 2024-2025 Gregory Lirent en-US + + WINIPC_UA_SERVER - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/winipc-ua.sln b/winipc-ua.sln new file mode 100644 index 0000000..b0d7501 --- /dev/null +++ b/winipc-ua.sln @@ -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