commit d6325ee129ddce4f25a4dbece39132b9dd503fcd Author: Gregory Lirent Date: Thu Jan 8 21:34:38 2026 +0300 fix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee8705c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.*/ +/bin/ +/obj/ +/Properties/ +/*.Development.json +/*.csproj.user diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6716d5d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Lib/winipc-ua"] + path = Lib/winipc-ua + url = https://dev.lirent.ru/lirent/winipc-ua.git diff --git a/Config/AppConfig.cs b/Config/AppConfig.cs new file mode 100644 index 0000000..d2ae17e --- /dev/null +++ b/Config/AppConfig.cs @@ -0,0 +1,7 @@ +namespace WinIPC.Config +{ + public class AppConfig + { + public IPCServiceOptions IPCService { get; set; } = new IPCServiceOptions(); + } +} diff --git a/Lib/winipc-ua b/Lib/winipc-ua new file mode 160000 index 0000000..4a8b517 --- /dev/null +++ b/Lib/winipc-ua @@ -0,0 +1 @@ +Subproject commit 4a8b51715c24f2fa0eb0313c56c6a565abe89806 diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c91e7b2 --- /dev/null +++ b/Program.cs @@ -0,0 +1,327 @@ +using System.IO; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text.Json; +using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; +//using WinIPC.Utils + +using Google.Protobuf; +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using WinIPC.Models; // Предполагается, что сгенерированные Protobuf-классы находятся здесь + +namespace IpcCliClient +{ + class Program + { + private static readonly string _pipeName = "Global\\ProcessMonitoringService.UserAgent.Pipe"; + private static uint _requestId = 0; + + static async Task Main(string[] args) + { + Console.WriteLine("Клиентское приложение для тестирования IPC."); + Console.WriteLine("Введите команду (например, get_windows) или 'exit' для выхода."); + Console.WriteLine("---------------------------------------------"); + + while (true) + { + Console.Write("> "); + string command = Console.ReadLine()?.Trim() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(command)) + { + continue; + } + + if (command.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + try + { + await ExecuteCommand(command); + } + catch (Exception ex) + { + Console.WriteLine($"Ошибка: {ex.Message}"); + } + } + + Console.WriteLine("Приложение завершено."); + } + + /// + /// Выполняет команду, введенную пользователем. + /// + private static async Task ExecuteCommand(string command) + { + Request request = new Request(); + string responseMessage = string.Empty; + bool handled = true; + + switch (command.ToLowerInvariant()) + { + case "get_windows": + request.Id = ++_requestId; + request.Type = CommandType.GetWindowsInfo; + Console.WriteLine("Отправка запроса на получение списка окон..."); + responseMessage = "Список окон:"; + break; + + // TODO: Добавить другие команды (get_screenshot, get_pixel_color, input_action) + // как только будут определены их структуры запросов/ответов. + + default: + Console.WriteLine($"Неизвестная команда: '{command}'"); + handled = false; + break; + } + + if (handled) + { + await SendAndReceive(request, responseMessage); + } + } + + /// + /// Отправляет запрос и получает ответ от сервера. + /// + private static async Task SendAndReceive(Request request, string successMessage) + { + try + { + using (var pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut)) + { + Console.WriteLine("Подключение к IPC серверу..."); + await pipeClient.ConnectAsync(5000); // Таймаут 5 секунд + Console.WriteLine("Подключено."); + + // Сериализация и отправка запроса + byte[] requestBytes = request.ToByteArray(); + await WriteMessageAsync(pipeClient, requestBytes, CancellationToken.None); + + // Чтение ответа + byte[] responseBytes = await ReadMessageAsync(pipeClient, CancellationToken.None); + var response = Response.Parser.ParseFrom(responseBytes); + + // Обработка ответа + if (response.Success) + { + Console.WriteLine($"Успешно: {successMessage}"); + if (response.Type == CommandType.GetWindowsInfo && response.Payload != null) + { + var windowsResponse = WindowsResponse.Parser.ParseFrom(response.Payload); + Console.WriteLine($"Найдено {windowsResponse.Data.Count} окон:"); + foreach (var window in windowsResponse.Data) + { + Console.WriteLine($" - HWND: {window.Hwnd}, PID: {window.Pid}, Title: '{window.Title}'"); + } + } + else + { + Console.WriteLine($"Получен успешный ответ. Сообщение: {response.Message}"); + } + } + else + { + Console.WriteLine($"Ошибка ответа: {response.Message}"); + } + } + } + catch (TimeoutException) + { + Console.WriteLine("Ошибка: Таймаут подключения к серверу."); + } + catch (Exception ex) + { + Console.WriteLine($"Ошибка при взаимодействии с сервером: {ex.Message}"); + } + } + + /// + /// Читает сообщение из pipe. + /// + private static async Task ReadMessageAsync(PipeStream pipe, CancellationToken cTok) + { + // Чтение префикса длины (4 байта) + byte[] lengthBytes = new byte[4]; + int bytesRead = await pipe.ReadAsync(lengthBytes, 0, 4, cTok); + if (bytesRead < 4) + { + throw new EndOfStreamException("Поток закрыт или достигнут конец потока при чтении длины сообщения."); + } + + int messageLength = BitConverter.ToInt32(lengthBytes, 0); + + if (messageLength <= 0) + { + throw new InvalidDataException($"Получена некорректная длина сообщения: {messageLength}."); + } + + // Чтение самого сообщения + byte[] messageBytes = new byte[messageLength]; + bytesRead = 0; + while (bytesRead < messageLength) + { + int read = await pipe.ReadAsync(messageBytes, bytesRead, messageLength - bytesRead, cTok); + if (read == 0) + { + throw new EndOfStreamException($"Поток закрыт или достигнут конец потока при чтении полезной нагрузки. Ожидалось {messageLength} байт, прочитано {bytesRead}."); + } + bytesRead += read; + } + + return messageBytes; + } + + /// + /// Записывает сообщение в pipe. + /// + private static async Task WriteMessageAsync(PipeStream pipe, byte[] messageBytes, CancellationToken cTok) + { + byte[] lengthPrefix = BitConverter.GetBytes(messageBytes.Length); + + await pipe.WriteAsync(lengthPrefix, 0, lengthPrefix.Length, cTok); + await pipe.WriteAsync(messageBytes, 0, messageBytes.Length, cTok); + await pipe.FlushAsync(cTok); + } + } +} + +/* +public class Datagram +{ + public IPCWindowInfo Info; + public string Title { get; set; } = String.Empty; + public int Hwnd { get => Info.Hwnd; } + public uint Pid { get => Info.Pid; } + public uint ThreadId { get => Info.ThreadId; } + public bool IsActive { get => Info.IsActive; } + public int CursorX { get => Info.CursorX; } + public int CursorY { get => Info.CursorY; } + public int ContentWidth { get => Info.ContentWidth; } + public int ContentHeight { get => Info.ContentHeight; } + public int WindowX { get => Info.WindowX; } + public int WindowY { get => Info.WindowY; } + public int WindowWidth { get => Info.WindowWidth; } + public int WindowHeight { get => Info.WindowHeight; } + +} + +public class Program +{ + public static async Task Main(string[] args) + { + Console.WriteLine("IPC Client started. Press any key to send a 'GetWindows' request..."); + Console.ReadKey(); + + await SendGetWindowsRequest(); + + Console.WriteLine("\nPress any key to exit."); + Console.ReadKey(); + } + + private static async Task SendGetWindowsRequest() + { + try + { + using (var pipe = new NamedPipeClientStream( + ".", // Сервер находится на локальной машине + IPC.PipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous, + TokenImpersonationLevel.None, + HandleInheritability.None)) + { + Console.WriteLine($"Connecting to pipe: {IPC.PipeName}..."); + await pipe.ConnectAsync(5000); // Таймаут 5 секунд + Console.WriteLine("Connected to pipe!"); + + IPCPayload payload; + +#if WINDOWS + if (IPC.SerializeIPC(new IPCHeader { Type = IPCType.ListWindows }, 0, out var data) > 0) + { + Console.WriteLine($"Writing to pipe..."); + if (await IPC.WriteMessageAsync(pipe, data, CancellationToken.None)) + { + Console.WriteLine($"Writing successful"); + Console.WriteLine($"Reading from pipe..."); + if ((payload = await IPC.ReadMessageAsync(pipe, CancellationToken.None)).Header.Type == (IPCType.Response | IPCType.ListWindows)) + { + Console.WriteLine($"Reading successful"); + var s = (IEnumerable?)payload.Data; + var d = new List(); + + if (s != null) { + foreach (var p in s) + { + d.Add(new Datagram { Info = p.Info, Title = p.Title }); + } + } + + Console.WriteLine($"Received response: {JsonSerializer.Serialize(d)}"); + } + else + { + Console.WriteLine($"Not received! ID: {payload.Header.ID}, Code: {payload.Header.Code}, Type: {payload.Header.Type}"); + } + } + } +#endif +#if IMAGE + if (IPC.SerializeIPC(new IPCHeader { Type = IPCType.GetScreenshot }, 0, out var d1) > 0 && IPC.SerializeIPC(new IPCScreenshotRequest { Hwnd = 66982 }, 0, out var d2) > 0) + { + byte[] data = new byte[d1.Length + d2.Length]; + + Buffer.BlockCopy(d1, 0, data, 0, d1.Length); + Buffer.BlockCopy(d2, 0, data, d1.Length, d2.Length); + + Console.WriteLine($"Writing to pipe..."); + if (await IPC.WriteMessageAsync(pipe, data, CancellationToken.None)) + { + Console.WriteLine($"Writing successful"); + Console.WriteLine($"Reading from pipe..."); + if ((payload = await IPC.ReadMessageAsync(pipe, CancellationToken.None)).Header.Type == (IPCType.Response | IPCType.GetScreenshot)) + { + Console.WriteLine($"Reading successful"); + var s = (IPCScreenshotPayload?)payload.Data; + var d = new List(); + + if (s != null) + { + using (FileStream fs = new FileStream("C:\\Users\\lirent\\test.png", FileMode.Create, FileAccess.Write, FileShare.None)) + { + await fs.WriteAsync(s.Value.Image, 0, s.Value.Image.Length); + } + Console.WriteLine("See C:\\Users\\lirent\\test.png"); + } + } + else + { + Console.WriteLine($"Not received! ID: {payload.Header.ID}, Code: {payload.Header.Code}, Type: {payload.Header.Type}"); + } + } + } +#endif + + } + } + catch (TimeoutException) + { + Console.WriteLine("Connection to pipe timed out. Ensure UserSessionAgent is running."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + Console.WriteLine(ex.ToString()); + } + } +} +*/ \ No newline at end of file diff --git a/Services/IPCClientService.cs b/Services/IPCClientService.cs new file mode 100644 index 0000000..ba3ca81 --- /dev/null +++ b/Services/IPCClientService.cs @@ -0,0 +1,267 @@ +/* This software is licensed by the MIT License, see LICENSE file */ +/* Copyright © 2024-2025 Gregory Lirent */ + +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 IPCClientService : 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 IPCClientService(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; + } + } +} diff --git a/winipc-ua-test.csproj b/winipc-ua-test.csproj new file mode 100644 index 0000000..8c314ec --- /dev/null +++ b/winipc-ua-test.csproj @@ -0,0 +1,34 @@ + + + + net8.0-windows + enable + + enable + true + + WinIPC + UserAgentTest + + 1.0.0.0 + 1.0.0.0 + 0.1.0 + OpenSource + en-US + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/winipc-ua-test.sln b/winipc-ua-test.sln new file mode 100644 index 0000000..5eb328a --- /dev/null +++ b/winipc-ua-test.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-test", "winipc-ua-test.csproj", "{25044D91-C896-2DFB-8299-FBDF8F1D02EA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25044D91-C896-2DFB-8299-FBDF8F1D02EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25044D91-C896-2DFB-8299-FBDF8F1D02EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25044D91-C896-2DFB-8299-FBDF8F1D02EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25044D91-C896-2DFB-8299-FBDF8F1D02EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7EC6842F-AE05-44E2-82E2-B13E5A19C4B5} + EndGlobalSection +EndGlobal