This commit is contained in:
Gregory Lirent 2026-01-08 21:34:38 +03:00
commit d6325ee129
8 changed files with 670 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/.*/
/bin/
/obj/
/Properties/
/*.Development.json
/*.csproj.user

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Lib/winipc-ua"]
path = Lib/winipc-ua
url = https://dev.lirent.ru/lirent/winipc-ua.git

7
Config/AppConfig.cs Normal file
View File

@ -0,0 +1,7 @@
namespace WinIPC.Config
{
public class AppConfig
{
public IPCServiceOptions IPCService { get; set; } = new IPCServiceOptions();
}
}

1
Lib/winipc-ua Submodule

@ -0,0 +1 @@
Subproject commit 4a8b51715c24f2fa0eb0313c56c6a565abe89806

327
Program.cs Normal file
View File

@ -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("Приложение завершено.");
}
/// <summary>
/// Выполняет команду, введенную пользователем.
/// </summary>
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);
}
}
/// <summary>
/// Отправляет запрос и получает ответ от сервера.
/// </summary>
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}");
}
}
/// <summary>
/// Читает сообщение из pipe.
/// </summary>
private static async Task<byte[]> 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;
}
/// <summary>
/// Записывает сообщение в pipe.
/// </summary>
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<IPCWindowInfoPayload>?)payload.Data;
var d = new List<Datagram>();
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<Datagram>();
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());
}
}
}
*/

View File

@ -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<IPCClientService> _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 IPCClientService(ILogger<IPCClientService> 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;
}
}
}

34
winipc-ua-test.csproj Normal file
View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>WinIPC</RootNamespace>
<AssemblyName>UserAgentTest</AssemblyName>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>0.1.0</Version>
<Company>OpenSource</Company>
<NeutralLanguage>en-US</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="System.Management" 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="Lib\winipc-ua\Models\ipc.proto" GrpcServices="None" ProtoRoot="." />
</ItemGroup>
</Project>

25
winipc-ua-test.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-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