diff --git a/src/NuGet.Config b/NuGet.Config similarity index 100% rename from src/NuGet.Config rename to NuGet.Config diff --git a/README.md b/README.md index 587059e6..627b3a22 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,290 @@ -[![Build Status](https://uipath.visualstudio.com/CoreIpc/_apis/build/status/CI?branchName=master)](https://uipath.visualstudio.com/CoreIpc/_build/latest?definitionId=637&branchName=master) -[![MyGet (dev)](https://img.shields.io/badge/CoreIpc-Preview-brightgreen)](https://uipath.visualstudio.com/Public.Feeds/_packaging?_a=package&feed=UiPath-Internal&view=versions&package=UiPath.CoreIpc&protocolType=NuGet) -# CoreIpc -WCF-like service model API for communication over named pipes, TCP and web sockets. .NET and [Node.js and Web](src/Clients/js) clients. -- async -- json serialization -- DI integration -- cancellation -- timeouts -- callbacks -- one way calls (all methods that return non-generic `Task`) -- automatic reconnect -- interception -- configurable task scheduler -- client authentication and impersonation -- access to the underlying transport with `Stream` parameters -- SSL - -Check [the tests](https://github.com/UiPath/CoreIpc/blob/master/src/UiPath.CoreIpc.Tests/) and the sample. -```C# -// configure and start the server -_ = new ServiceHostBuilder(serviceProvider) - .UseNamedPipes(new NamedPipeSettings("computing")) - .AddEndpoint() - .Build() - .RunAsync(); -// configure the client -var computingClient = - new NamedPipeClientBuilder("computing") - .Build(); -// call a remote method -var result = await computingClient.AddFloat(1, 4, cancellationToken); -``` -# UiPath.Rpc -[![Build Status](https://uipath.visualstudio.com/CoreIpc/_apis/build/status/CI?branchName=master)](https://uipath.visualstudio.com/CoreIpc/_build/latest?definitionId=3428&branchName=master) -[![MyGet (dev)](https://img.shields.io/badge/UiPath.Rpc-Preview-brightgreen)](https://uipath.visualstudio.com/Public.Feeds/_packaging?_a=package&feed=UiPath-Internal&view=versions&package=UiPath.Rpc&protocolType=NuGet) - -https://github.com/UiPath/coreipc/tree/master/UiPath.Rpc -A more efficient version based on MessagePack. -# Debug using Source Link -[Preview builds setup](https://docs.microsoft.com/en-us/azure/devops/pipelines/artifacts/symbols?view=azure-devops#set-up-visual-studio). \ No newline at end of file +# UiPath.Ipc + +[![Build Status](https://uipath.visualstudio.com/CoreIpc/_apis/build/status/CI?branchName=master)](https://uipath.visualstudio.com/CoreIpc/_build?definitionId=637) +[![NuGet Package](https://img.shields.io/badge/NuGet-UiPath.Ipc-blue)](https://uipath.visualstudio.com/Public.Feeds/_artifacts/feed/UiPath-Internal/NuGet/UiPath.Ipc/overview/2.5.1-20250714-01) +[![NPM Package](https://img.shields.io/badge/NPM-coreipc-red)](https://github.com/UiPath/coreipc/pkgs/npm/coreipc) +[![NPM Web Package](https://img.shields.io/badge/NPM-coreipc--web-red)](https://github.com/UiPath/coreipc/pkgs/npm/coreipc-web) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +> **Lightweight RPC framework** enabling bidirectional communication with interface-based contracts over **Named Pipes, TCP/IP, and WebSockets**. Supports .NET servers/clients and [Node.js/Web clients](src/Clients/js). + +## 🚀 Features + +- **🔄 Asynchronous**: Fully async/await compatible API +- **🔥 One-way Calls**: Fire-and-forget methods (methods returning non-generic `Task`) +- **📞 Callbacks**: Bidirectional communication with callback interfaces +- **⚡ Task Scheduling**: Configurable task scheduler support +- **📡 Multiple Transports**: Named Pipes, TCP/IP, WebSockets or custom transport +- **🔒 Security**: Client authentication and impersonation for Named Pipes +- **📦 JSON Serialization**: Built-in JSON serialization with Newtonsoft.Json +- **🏗️ DI Support**: Integration with Microsoft.Extensions.DependencyInjection +- **⏰ Cancellation & Timeouts**: Comprehensive cancellation token and timeout support +- **🔄 Auto-reconnect**: Broken connections are re-established transparently; you keep using the same proxy instance. +- **🛡️ Interception**: BeforeConnect, BeforeIncommingCall and BeforeOutgoingCall interception capabilities +- **📊 Stream Access**: Direct access to underlying transport streams +- **🌐 Cross-platform**: .NET 6, .NET Framework 4.6.1, and .NET 6 Windows support + +## 📦 Installation + +```bash +dotnet add package UiPath.Ipc +``` + +## 🏃‍♂️ Quick Start + +### 1. Define Your Service Contract + +```csharp +public interface IComputingService +{ + Task AddFloats(float x, float y, Message m = null!, CancellationToken ct = default); + Task Wait(TimeSpan duration, CancellationToken ct = default); +} + +public interface IComputingCallback +{ + Task GetThreadName(); +} +``` + +### 2. Implement Your Service + +```csharp +public sealed class ComputingService : IComputingService +{ + public async Task AddFloats(float a, float b, Message m = null!, CancellationToken ct = default) + { + return a + b; + } + + public async Task Wait(TimeSpan duration, CancellationToken ct = default) + { + await Task.Delay(duration, ct); + return true; + } +} +``` + +### 3. Create and Start the Server + +> Creating a server is done by instantiating the `IpcServer` class, setting its properties and calling the `Start` method. + +```csharp +await using var serviceProvider = new ServiceCollection() + .AddScoped() + .BuildServiceProvider(); + +await using var server = new IpcServer +{ + Transport = new NamedPipeServerTransport { PipeName = "computing" }, + ServiceProvider = serviceProvider, + Endpoints = new() { typeof(IComputingService) } +}; +server.Start(); +await server.WaitForStart(); +``` + +### 3. Create the Client + +> Creating a client is done by 1st implementing all the callback interfaces you'll want to expose as a client: + +```csharp +public sealed class ComputingCallback : IComputingCallback +{ + public Task GetThreadName() => Task.FromResult(Thread.CurrentThread.Name); +} +``` + +and then instantiating the `IpcClient`, setting its properties, obtaining a proxy via the `GetProxy` method and using that proxy. + +```csharp +await using var serviceProvider = new ServiceCollection() + .AddScoped() + .BuildServiceProvider(); + +var client = new IpcClient +{ + Transport = new NamedPipeClientTransport { PipeName = "computing" }, + ServiceProvider = serviceProvider, + Callbacks = new() { typeof(IComputingCallback) } +} + +var computingService = client.GetProxy(); +var three = await computingService.AddFloats(1, 2); +``` + +## 🔧 Advanced Features + +### Callbacks and Bidirectional Communication + +```csharp +public class ComputingCallback : IComputingCallback +{ + public async Task GetThreadName() + { + return Thread.CurrentThread.Name ?? "Unknown"; + } + + public async Task AddInts(int x, int y) + { + return x + y; + } +} + +// Server can call back to client +public async Task GetCallbackThreadName(TimeSpan waitOnServer, Message message, CancellationToken cancellationToken) +{ + await Task.Delay(waitOnServer, cancellationToken); + return await message.Client.GetCallback().GetThreadName(); +} +``` + +### Dependency Injection Integration + +```csharp +var services = new ServiceCollection() + .AddLogging(builder => builder.AddConsole()) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + +var ipcServer = new IpcServer +{ + ServiceProvider = services, + // ... other configuration +}; +``` + +### Custom Task Scheduling + +```csharp +var ipcServer = new IpcServer +{ + Scheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler, + // ... other configuration +}; +``` + +## 🧩 Notable Types + +### 1. The `IpcServer`, `IpcClient` and `IpcBase` classes. + +> This hierarchy is used for creating and hosting servers and clients respectively. + +> ```csharp +> public abstract class IpcBase { ... } +> public sealed class IpcServer : IpcBase { ... } +> public sealed class IpcClient : IpcBase { ... } +> ``` + +![IpcServer and IpcClient diagram](readme/diagram.svg) + +#### i. `IpcBase` + +> This class defines the settings shared between servers and clients. + +| Property | Type | Notes | +| -------- | ---- | ----- | +| ServiceProvider | IServiceProvider? | **Optional**, defaults to **null**: Resolves services when handling incomming calls. | +| Scheduler | TaskScheduler? | **Optional**, defaults to the thread pool: Schedules incomming calls. | +| RequestTimeout | TimeSpan? | **Optional**, defaults to infinity: Interval after which the honoring of requests will time out. | + + +#### ii. `IpcServer` + +**Declared properties** + +| Property | Type | Notes | +| -------- | ---- | ----- | +| Endpoints | ContractCollection | **Required**: The collection of `ContractSettings`, which specifies the services to be exposed over Ipc. | +| Transport | ServerTransport | **Required**: The server's transport, meaning whether it accepts connection over Named Pipes, TCP/IP, WebSockets, or a custom communication mechanism. | + +**Methods** + +| Method | Description | +| ------ | ----------- | +| `void Start()` | This method starts hosting the current `IpcServer` instance, meaning that it's imminent the transport will start listening and accepting connections, and those connections' calls will start to be honored.

It's thread-safe, idempotent and fire&forget in nature, meaning it doesn't wait for the listener to become active. Further changes to the otherwise mutable `IpcServer` instance have no effect on the listener's settings or its exposed service collection.

Exceptions:
- `InvalidOperationException`: wrong configurations, such a `null` or invalid transport.
- `ObjectDisposedException`: the `IpcServer` instance had been disposed. | +| `Task WaitForStart()` | This method calls `Start` and then awaits for the connection accepter to start. It's thread-safe and idempotent. | +| `ValueTask DisposeAsync()` | Stops the connection accepter and cancels all active connections before completing the returned `ValueTask`. | + +
+ +#### iii. `IpcClient` + +**Declared properties* + +| Property | Type | Notes | +| -------- | ---- | ----- | +| Callbacks | ContractCollection | **Optional**: The collection of `ContractSettings`, which specifies the services to be exposed over Ipc **as callbacks**. | +| Transport | ClientTransport | **Required**: The client's transport, meaning whether it connects to the server over Named Pipes, TCP/IP, WebSockets, or a custom communication mechanism. | + +**Methods** + +| Method | Notes | +| ------ | ----- | +| `TProxy GetProxy() where TProxy : class` | Returns an Ipc proxy of the specified type, which is the gateway for remote calling. This method is idempotent, meaning that it will cache its result. | + +### 2. The `ContractCollection` and `ContractSettings` classes. + +#### i. `ContractCollection` + +> `ContractCollection` is a type-safe collection that holds `ContractSettings` instances, mapping service interface types to their configuration. It implements `IEnumerable` and provides convenient `Add` methods for different scenarios. + +**Add Methods:** + +| Method | Description | +| ------ | ----------- | +| `Add(Type contractType)` | Adds a contract type that will be resolved from the service provider when needed (deferred resolution). | +| `Add(Type contractType, object? instance)` | Adds a contract type with a specific service instance. If `instance` is `null`, uses deferred resolution. | +| `Add(ContractSettings endpointSettings)` | Adds a pre-configured `ContractSettings` instance directly. | + +#### ii. `ContractSettings` + +> `ContractSettings` represents the configuration for a single service contract, including how the service instance is created/resolved, task scheduling, and call interception. + +**Properties:** + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Scheduler` | `TaskScheduler?` | **Optional**: Custom task scheduler for this specific contract. Inherits from `IpcBase.Scheduler` if not set. | +| `BeforeIncomingCall` | `BeforeCallHandler?` | **Optional**: Interceptor called before each incoming method call on this contract. | + +**Constructors:** + +| Constructor | Description | +| ----------- | ----------- | +| `ContractSettings(Type contractType, object? serviceInstance = null)` | Creates settings for a contract type with optional direct service instance. If `serviceInstance` is `null`, uses deferred resolution. | +| `ContractSettings(Type contractType, IServiceProvider serviceProvider)` | Creates settings for a contract type with explicit service provider for dependency injection. | + +**Service Resolution Strategies:** + +- **Direct Instance**: When you provide a service instance, that exact instance is used for all calls +- **Deferred Resolution**: When no instance is provided, the service is resolved from the `IpcServer`'s `ServiceProvider` when needed +- **Injected Resolution**: When you provide a specific `IServiceProvider`, services are resolved from that provider + +**Usage Examples:** + +```csharp +// Direct instance +var settings1 = new ContractSettings(typeof(IComputingService), new ComputingService()); + +// Deferred resolution (will use IpcServer.ServiceProvider) +var settings2 = new ContractSettings(typeof(IComputingService)); + +// Custom service provider +var customProvider = new ServiceCollection() + .AddTransient() + .BuildServiceProvider(); +var settings3 = new ContractSettings(typeof(IComputingService), customProvider); + +// With advanced configuration +var settings4 = new ContractSettings(typeof(IComputingService)) +{ + Scheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler, + BeforeIncomingCall = async (callInfo, ct) => + { + Console.WriteLine($"Calling {callInfo.Method.Name}"); + } +}; +``` diff --git a/readme/diagram.svg b/readme/diagram.svg new file mode 100644 index 00000000..598d5403 --- /dev/null +++ b/readme/diagram.svg @@ -0,0 +1,3 @@ + + +
IpcServer
IpcServer
IpcClient
IpcClient
IpcBase
IpcBase
RequestTimeout: TimeSpan?
RequestTimeout: TimeSpan?
ServiceProvider: IServiceProvider?
ServiceProvider: IServiceProvider?
Scheduler: TaskScheduler?
Scheduler: TaskScheduler?
Callbacks: ContractCollection
Callbacks: ContractCollection
Endpoints: ContractCollection
Endpoints: ContractCollection
Transport: ClientTransport
Transport: ClientTransport
Transport: ServerTransport
Transport: ServerTransport
Logger: ILogger?
Logger: ILogger?
BeforeConnect: BeforeConnectHandler?
BeforeConnect: BeforeConnectHandler?
BeforeOutgoingCall: BeforeCallHandler?
BeforeOutgoingCall: BeforeCallHandler?
diff --git a/src/CI/azp-dotnet-dist.yaml b/src/CI/azp-dotnet-dist.yaml index bafcc806..8cfffb63 100644 --- a/src/CI/azp-dotnet-dist.yaml +++ b/src/CI/azp-dotnet-dist.yaml @@ -16,7 +16,7 @@ steps: - task: DotNetCoreCLI@2 displayName: 'dotnet push to UiPath-Internal' - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) + condition: succeeded() inputs: command: push packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' diff --git a/src/CI/azp-dotnet.yaml b/src/CI/azp-dotnet.yaml index 0af4fc84..4a9c62a2 100644 --- a/src/CI/azp-dotnet.yaml +++ b/src/CI/azp-dotnet.yaml @@ -1,4 +1,10 @@ steps: + - task: DotNetCoreCLI@2 + displayName: '$(Label_DotNet) Restore, build and pack' + inputs: + projects: '$(DotNet_SessionSolution)' + arguments: '--configuration $(DotNet_BuildConfiguration) -p:Version="$(FullVersion)" -p:DefineConstantsEx="CI"' + - task: DotNetCoreCLI@2 displayName: '$(Label_DotNet) Run unit tests' inputs: @@ -6,4 +12,4 @@ steps: projects: '$(DotNet_SessionSolution)' publishTestResults: true testRunTitle: '.NET tests' - arguments: ' --configuration $(DotNet_BuildConfiguration) --logger "console;verbosity=detailed" -p:Version="$(FullVersion)" -p:DefineConstantsEx="CI"' \ No newline at end of file + arguments: '--no-build --configuration $(DotNet_BuildConfiguration) --logger "console;verbosity=detailed" -p:Version="$(FullVersion)" -p:DefineConstantsEx="CI"' \ No newline at end of file diff --git a/src/CI/azp-initialization.yaml b/src/CI/azp-initialization.yaml index 3cc0d0a3..8ecc5785 100644 --- a/src/CI/azp-initialization.yaml +++ b/src/CI/azp-initialization.yaml @@ -1,4 +1,23 @@ steps: +# Runtime version should match with SDK version. +# The runtime should be the one that is contained in SDK. +# https://dotnet.microsoft.com/en-us/download/dotnet/6.0 + - powershell: | + Write-Host "##vso[task.setvariable variable=DotnetRuntimeVersion;]8.0.8" + Write-Host "##vso[task.setvariable variable=DOTNET_NOLOGO;]true" + displayName: 'Use .NET Runtime 8.0.8' + + - task: UseDotNet@2 + displayName: 'Use .NET SDK 6.0.317' + inputs: + packageType: 'sdk' + version: '6.0.317' + + - task: UseDotNet@2 + displayName: 'Use .NET SDK 8.0.400' + inputs: + packageType: 'sdk' + version: 8.0.400 # Read $(Version) from the UiPath.CoreIpc.csproj file - powershell: | diff --git a/src/CI/azp-nodejs.yaml b/src/CI/azp-nodejs.yaml index 52f7d934..ee14c6bf 100644 --- a/src/CI/azp-nodejs.yaml +++ b/src/CI/azp-nodejs.yaml @@ -48,7 +48,7 @@ inputs: workingDirectory: $(NodeJS_ProjectPath) script: 'npm test' - + - task: PublishTestResults@2 displayName: 'Publish Web Test Results' condition: succeededOrFailed() diff --git a/src/CI/azp-start.yaml b/src/CI/azp-start.yaml index d659af93..ca321e0b 100644 --- a/src/CI/azp-start.yaml +++ b/src/CI/azp-start.yaml @@ -10,7 +10,7 @@ variables: DotNet_MainProjectName: 'UiPath.CoreIpc' DotNet_MainProjectPath: './src/UiPath.CoreIpc/UiPath.CoreIpc.csproj' DotNet_ArtifactName: 'NuGet package' - + NodeJS_DotNet_BuildConfiguration: 'Debug' NodeJS_ProjectPath: './src/Clients/js' NodeJS_ArchivePath: './src/Clients/js/dist/pack/nodejs.zip' @@ -23,32 +23,32 @@ stages: - stage: Build displayName: '🏭 Build' jobs: - # The following 3 jobs will run in parallel: - - job: - displayName: '.NET on Windows' - pool: + # The following 3 jobs will run in parallel: + - job: + displayName: '.NET on Windows' + pool: vmImage: 'windows-2022' - steps: - - template: azp-initialization.yaml - - template: azp-dotnet.yaml - - template: azp-dotnet-dist.yaml - - - job: - displayName: 'node.js on Windows' - pool: + steps: + - template: azp-initialization.yaml + - template: azp-dotnet.yaml + - template: azp-dotnet-dist.yaml + + - job: + displayName: 'node.js on Windows' + pool: vmImage: 'windows-2022' - steps: - - template: azp-initialization.yaml - - template: azp-nodejs.yaml - - template: azp-nodejs-dist.yaml - - - job: - displayName: 'node.js on Ubuntu' - pool: - vmImage: 'ubuntu-20.04' - steps: - - template: azp-initialization.yaml - - template: azp-nodejs.yaml + steps: + - template: azp-initialization.yaml + - template: azp-nodejs.yaml + - template: azp-nodejs-dist.yaml + + - job: + displayName: 'node.js on Ubuntu' + pool: + vmImage: 'ubuntu-22.04' + steps: + - template: azp-initialization.yaml + - template: azp-nodejs.yaml - stage: Publish displayName: 🚚 Publish diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop.sln b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop.sln index e96e6ecc..13bc8866 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop.sln +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30320.27 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc.NodeInterop", "UiPath.CoreIpc.NodeInterop\UiPath.CoreIpc.NodeInterop.csproj", "{B514D2A2-B8ED-4A2A-BDE7-42F74A316FBE}" EndProject diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Contracts.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Contracts.cs index 0a725bab..d86c0d8e 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Contracts.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Contracts.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace UiPath.CoreIpc.NodeInterop; +namespace UiPath.Ipc.NodeInterop; internal static class Contracts { diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs index 17f23329..cba5d22e 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs @@ -3,18 +3,20 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using UiPath.CoreIpc.NamedPipe; -using UiPath.CoreIpc.WebSockets; +using UiPath.Ipc.Transport.NamedPipe; +using UiPath.Ipc.Transport.WebSocket; -namespace UiPath.CoreIpc.NodeInterop; +namespace UiPath.Ipc.NodeInterop; using static Contracts; using static ServiceImpls; using static Signalling; +using static UiPath.Ipc.NodeInterop.Extensions; class Program { @@ -61,6 +63,11 @@ static async Task Main( static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSecondsPowerOnDelay) { + if (pipeName is null && webSocketUrl is null) + { + throw new ArgumentException($"At least one of {nameof(pipeName)} or {nameof(webSocketUrl)} must be specified."); + } + if (maybeSecondsPowerOnDelay is { } secondsPowerOnDelay) { await Task.Delay(TimeSpan.FromSeconds(secondsPowerOnDelay)); @@ -71,7 +78,6 @@ static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSec var sp = services .AddLogging() - .AddIpc() .AddSingleton() .AddSingleton() .AddSingleton() @@ -79,18 +85,13 @@ static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSec .AddSingleton() .BuildServiceProvider(); - var serviceHost = new ServiceHostBuilder(sp) - .UseNamedPipesAndOrWebSockets(pipeName, webSocketUrl) - .AddEndpoint() - .AddEndpoint() - .AddEndpoint() - .AddEndpoint() - .AddEndpoint() - .Build(); - var thread = new AsyncContextThread(); thread.Context.SynchronizationContext.Send(_ => Thread.CurrentThread.Name = "GuiThread", null); - var sched = thread.Context.Scheduler; + var scheduler = thread.Context.Scheduler; + + var ipcServers = EnumerateServerTransports(pipeName, webSocketUrl) + .Select(CreateAndStartIpcServer) + .ToArray(); _ = Task.Run(async () => { @@ -98,7 +99,6 @@ static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSec { await using var sp = new ServiceCollection() .AddLogging() - .AddIpc() .BuildServiceProvider(); var callback = new Arithmetic(); @@ -107,25 +107,45 @@ IEnumerable EnumeratePings() { if (webSocketUrl is not null) { - yield return new WebSocketClientBuilder(uri: new(webSocketUrl), sp) - .RequestTimeout(TimeSpan.FromHours(5)) - .CallbackInstance(callback) - .Build() - .Ping(); + yield return new IpcClient + { + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() + { + { typeof(IArithmetic), callback } + }, + Transport = new WebSocketClientTransport + { + Uri = new(webSocketUrl), + } + } + .GetProxy() + .Ping(); } if (pipeName is not null) { - yield return new NamedPipeClientBuilder(pipeName, sp) - .RequestTimeout(TimeSpan.FromHours(5)) - .CallbackInstance(callback) - .Build() - .Ping(); + yield return new IpcClient + { + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() + { + { typeof(IArithmetic), callback } + }, + Transport = new NamedPipeClientTransport() + { + PipeName = pipeName, + } + } + .GetProxy() + .Ping(); } } await Task.WhenAll(EnumeratePings()); - + Send(SignalKind.ReadyToConnect); } catch (Exception ex) @@ -135,50 +155,62 @@ IEnumerable EnumeratePings() } }); - await serviceHost.RunAsync(sched); - } - - private class Arithmetic : IArithmetic - { - public Task Sum(int x, int y) => Task.FromResult(x + y); - - public Task SendMessage(Message message) => Task.FromResult(true); - } -} - -internal static class Extensions -{ - public static ServiceHostBuilder UseNamedPipesAndOrWebSockets(this ServiceHostBuilder builder, string? pipeName, string? webSocketUrl) - { - if (pipeName is null && webSocketUrl is null) - { - throw new ArgumentOutOfRangeException(); - } + await Task.Delay(Timeout.InfiniteTimeSpan); - if (pipeName is not null) + IpcServer CreateAndStartIpcServer(ServerTransport transport) { - builder = builder.UseNamedPipes(new NamedPipeSettings(pipeName)); + var ipcServer = new IpcServer() + { + Endpoints = new() + { + typeof(IAlgebra), + typeof(ICalculus), + typeof(IBrittleService), + typeof(IEnvironmentVariableGetter), + typeof(IDtoService) + }, + Transport = transport, + ServiceProvider = sp, + Scheduler = scheduler + }; + ipcServer.Start(); + return ipcServer; } - if (webSocketUrl is not null) + + IEnumerable EnumerateServerTransports(string? pipeName, string? webSocketUrl) { - string url = CurateWebSocketUrl(webSocketUrl); - var accept = new HttpSysWebSocketsListener(url).Accept; - WebSocketSettings settings = new(accept); + if (pipeName is not null) + { + yield return new NamedPipeServerTransport() { PipeName = pipeName }; + } - builder = builder.UseWebSockets(settings); - } + if (webSocketUrl is not null) + { + string url = CurateWebSocketUrl(webSocketUrl); + var accept = new HttpSysWebSocketsListener(url).Accept; + yield return new WebSocketServerTransport() { Accept = accept }; + } - return builder; + static string CurateWebSocketUrl(string raw) + { + var builder = new UriBuilder(raw); + builder.Scheme = "http"; + return builder.ToString(); + } + } } - private static string CurateWebSocketUrl(string raw) + private class Arithmetic : IArithmetic { - var builder = new UriBuilder(raw); - builder.Scheme = "http"; - return builder.ToString(); + public Task Sum(int x, int y) => Task.FromResult(x + y); + + public Task SendMessage(Message message) => Task.FromResult(true); } +} +internal static class Extensions +{ public class HttpSysWebSocketsListener : IDisposable { HttpListener _httpListener = new(); diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs index 0a436080..e6de1f24 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -namespace UiPath.CoreIpc.NodeInterop; +namespace UiPath.Ipc.NodeInterop; using static Contracts; @@ -20,7 +20,7 @@ public async Task MultiplySimple(int x, int y) public async Task Multiply(int x, int y, Message message = default!) { - var arithmetic = message.GetCallback(); + var arithmetic = message.Client.GetCallback(); int result = 0; for (int i = 0; i < x; i++) @@ -32,7 +32,7 @@ public async Task Multiply(int x, int y, Message message = default!) } public async Task TestMessage(Message message) { - var arithmetic = message.GetCallback(); + var arithmetic = message.Client.GetCallback(); return await arithmetic.SendMessage(message); } diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Signalling.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Signalling.cs index aba44d91..76d667f4 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Signalling.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Signalling.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Converters; using System; -namespace UiPath.CoreIpc.NodeInterop; +namespace UiPath.Ipc.NodeInterop; internal static class Signalling { diff --git a/src/CoreIpc.sln b/src/CoreIpc.sln index b69f1d5f..5c3bd64e 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -3,41 +3,63 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31919.166 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IpcSample.ConsoleServer", "IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj", "{24A3C4D2-95A2-48D9-86F2-648879EC74F4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IpcSample.ConsoleClient", "IpcSample.ConsoleClient\IpcSample.ConsoleClient.csproj", "{8D54E62A-ECFF-4FFF-B9D1-DB343D456451}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc", "UiPath.CoreIpc\UiPath.CoreIpc.csproj", "{58200319-1F71-4E22-894D-7E69E0CD0B57}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc.Tests", "UiPath.CoreIpc.Tests\UiPath.CoreIpc.Tests.csproj", "{892424AE-4D3A-4984-914E-9423BE8D0212}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{676A208A-2F08-4749-A833-F8D2BCB1B147}" ProjectSection(SolutionItems) = preProject - NuGet.Config = NuGet.Config + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj = IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj + ..\NuGet.Config = ..\NuGet.Config + ..\README.md = ..\README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "Playground\Playground.csproj", "{F0365E40-DA73-4583-A363-89CBEF68A4C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiPath.CoreIpc.Extensions.Abstractions", "UiPath.CoreIpc.Extensions.Abstractions\UiPath.CoreIpc.Extensions.Abstractions.csproj", "{F519AE2B-88A6-482E-A6E2-B525F71F566D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiPath.CoreIpc.Extensions.BidirectionalHttp", "UiPath.CoreIpc.Extensions.BidirectionalHttp\UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj", "{CE41844B-0F4F-4A5D-9AF9-77B58BE6ECD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiPath.CoreIpc.Tests", "UiPath.CoreIpc.Tests\UiPath.CoreIpc.Tests.csproj", "{41D716D4-78FC-4325-A20F-DA5A52AD3275}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IpcSample.ConsoleClient", "IpcSample.ConsoleClient\IpcSample.ConsoleClient.csproj", "{2FF5D17E-E2E2-4A1E-B813-D681291EF3A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IpcSample.ConsoleServer", "IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj", "{3CC0CFD7-528E-4091-A89D-DDCA3C0C3FED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {24A3C4D2-95A2-48D9-86F2-648879EC74F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {24A3C4D2-95A2-48D9-86F2-648879EC74F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {24A3C4D2-95A2-48D9-86F2-648879EC74F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {24A3C4D2-95A2-48D9-86F2-648879EC74F4}.Release|Any CPU.Build.0 = Release|Any CPU - {8D54E62A-ECFF-4FFF-B9D1-DB343D456451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D54E62A-ECFF-4FFF-B9D1-DB343D456451}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D54E62A-ECFF-4FFF-B9D1-DB343D456451}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D54E62A-ECFF-4FFF-B9D1-DB343D456451}.Release|Any CPU.Build.0 = Release|Any CPU {58200319-1F71-4E22-894D-7E69E0CD0B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58200319-1F71-4E22-894D-7E69E0CD0B57}.Debug|Any CPU.Build.0 = Debug|Any CPU {58200319-1F71-4E22-894D-7E69E0CD0B57}.Release|Any CPU.ActiveCfg = Release|Any CPU {58200319-1F71-4E22-894D-7E69E0CD0B57}.Release|Any CPU.Build.0 = Release|Any CPU - {892424AE-4D3A-4984-914E-9423BE8D0212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {892424AE-4D3A-4984-914E-9423BE8D0212}.Debug|Any CPU.Build.0 = Debug|Any CPU - {892424AE-4D3A-4984-914E-9423BE8D0212}.Release|Any CPU.ActiveCfg = Release|Any CPU - {892424AE-4D3A-4984-914E-9423BE8D0212}.Release|Any CPU.Build.0 = Release|Any CPU + {F0365E40-DA73-4583-A363-89CBEF68A4C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0365E40-DA73-4583-A363-89CBEF68A4C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0365E40-DA73-4583-A363-89CBEF68A4C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0365E40-DA73-4583-A363-89CBEF68A4C6}.Release|Any CPU.Build.0 = Release|Any CPU + {F519AE2B-88A6-482E-A6E2-B525F71F566D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F519AE2B-88A6-482E-A6E2-B525F71F566D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F519AE2B-88A6-482E-A6E2-B525F71F566D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F519AE2B-88A6-482E-A6E2-B525F71F566D}.Release|Any CPU.Build.0 = Release|Any CPU + {CE41844B-0F4F-4A5D-9AF9-77B58BE6ECD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE41844B-0F4F-4A5D-9AF9-77B58BE6ECD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE41844B-0F4F-4A5D-9AF9-77B58BE6ECD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE41844B-0F4F-4A5D-9AF9-77B58BE6ECD1}.Release|Any CPU.Build.0 = Release|Any CPU + {41D716D4-78FC-4325-A20F-DA5A52AD3275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41D716D4-78FC-4325-A20F-DA5A52AD3275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41D716D4-78FC-4325-A20F-DA5A52AD3275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41D716D4-78FC-4325-A20F-DA5A52AD3275}.Release|Any CPU.Build.0 = Release|Any CPU + {2FF5D17E-E2E2-4A1E-B813-D681291EF3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FF5D17E-E2E2-4A1E-B813-D681291EF3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FF5D17E-E2E2-4A1E-B813-D681291EF3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FF5D17E-E2E2-4A1E-B813-D681291EF3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {3CC0CFD7-528E-4091-A89D-DDCA3C0C3FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CC0CFD7-528E-4091-A89D-DDCA3C0C3FED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CC0CFD7-528E-4091-A89D-DDCA3C0C3FED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CC0CFD7-528E-4091-A89D-DDCA3C0C3FED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CoreIpc.sln.startup.json b/src/CoreIpc.sln.startup.json new file mode 100644 index 00000000..56b56346 --- /dev/null +++ b/src/CoreIpc.sln.startup.json @@ -0,0 +1,63 @@ +/* + This is a configuration file for the SwitchStartupProject Visual Studio Extension + See https://github.com/ernstc/SwitchStartupProject2022/blob/main/Configuration.md +*/ +{ + /* Configuration File Version */ + "Version": 3, + + /* Create an item in the dropdown list for each project in the solution? */ + "ListAllProjects": true, + + /* + Dictionary of named configurations with one or multiple startup projects + and optional parameters like command line arguments and working directory. + Example: + + "MultiProjectConfigurations": { + "A + B (Ext)": { + "Projects": { + "MyProjectA": {}, + "MyProjectB": { + "CommandLineArguments": "1234", + "WorkingDirectory": "%USERPROFILE%\\test", + "StartExternalProgram": "c:\\myprogram.exe" + } + } + }, + "A + B": { + "Projects": { + "MyProjectA": {}, + "MyProjectB": { + "CommandLineArguments": "", + "WorkingDirectory": "", + "StartProject": true + } + } + }, + "D (Debug x86)": { + "BeginGroup": true, + "Projects": { + "MyProjectD": {} + }, + "SolutionConfiguration": "Debug", + "SolutionPlatform": "x86", + }, + "D (Release x64)": { + "Projects": { + "MyProjectD": {} + }, + "SolutionConfiguration": "Release", + "SolutionPlatform": "x64", + } + } + */ + "MultiProjectConfigurations": { + "Client + Server": { + "Projects": { + "IpcSample.ConsoleClient": {}, + "IpcSample.ConsoleServer": {} + } + } + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 00000000..dcabbeb6 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 00000000..4979abb6 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/src/IpcSample.ConsoleClient/Client.cs b/src/IpcSample.ConsoleClient/Client.cs index 36515be2..92f03237 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -1,13 +1,13 @@ -using System.Text; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.NamedPipe; -using Microsoft.Extensions.DependencyInjection; +using System.Text; +using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -class Client +internal static class Client { - static async Task Main(string[] args) + public static async Task Main(string[] args) { Console.WriteLine(typeof(int).Assembly); Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); @@ -30,62 +30,65 @@ await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => private static async Task RunTestsAsync(CancellationToken cancellationToken) { var serviceProvider = ConfigureServices(); - var callback = new ComputingCallback { Id = "custom made" }; - var computingClientBuilder = new NamedPipeClientBuilder("test", serviceProvider) - .SerializeParametersAsObjects().CallbackInstance(callback).AllowImpersonation().RequestTimeout(TimeSpan.FromSeconds(2)); + var callback = new ComputingCallback(); + var ipcClient = new IpcClient + { + Transport = new NamedPipeClientTransport { PipeName = "test", AllowImpersonation = true }, + Callbacks = new() { + { typeof(IComputingCallback), callback } + }, + ServiceProvider = serviceProvider, + RequestTimeout = TimeSpan.FromSeconds(2) + }; + var stopwatch = Stopwatch.StartNew(); int count = 0; try { - var computingClient = computingClientBuilder.ValidateAndBuild(); - var systemClient = - new NamedPipeClientBuilder("test") - .SerializeParametersAsObjects() - .RequestTimeout(TimeSpan.FromSeconds(2)) - .Logger(serviceProvider) - .AllowImpersonation() - .ValidateAndBuild(); + var computingClient = ipcClient.GetProxy(); + var systemClient = ipcClient.GetProxy(); + for (int i = 0; i < int.MaxValue; i++) { // test 1: call IPC service method with primitive types - float result1 = await computingClient.AddFloat(1.23f, 4.56f, cancellationToken); + float result1 = await computingClient.AddFloats(1.23f, 4.56f, cancellationToken); count++; Console.WriteLine($"[TEST 1] sum of 2 floating number is: {result1}"); // test 2: call IPC service method with complex types - ComplexNumber result2 = await computingClient.AddComplexNumber( - new ComplexNumber(0.1f, 0.3f), - new ComplexNumber(0.2f, 0.6f), cancellationToken); - Console.WriteLine($"[TEST 2] sum of 2 complexe number is: {result2.A}+{result2.B}i"); + ComplexNumber result2 = await computingClient.AddComplexNumbers( + new ComplexNumber { I = 0.1f, J = 0.3f }, + new ComplexNumber { I = 0.2f, J = 0.6f }, cancellationToken); + Console.WriteLine($"[TEST 2] sum of 2 complex numbers is: {result2}"); // test 3: call IPC service method with an array of complex types - ComplexNumber result3 = await computingClient.AddComplexNumbers(new[] - { - new ComplexNumber(0.5f, 0.4f), - new ComplexNumber(0.2f, 0.1f), - new ComplexNumber(0.3f, 0.5f), - }, cancellationToken); - Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3.A}+{result3.B}i", cancellationToken); + ComplexNumber result3 = await computingClient.AddComplexNumberList( + [ + new ComplexNumber{ I = 0.5f, J = 0.4f }, + new ComplexNumber{ I = 0.2f, J = 0.1f }, + new ComplexNumber{ I = 0.3f, J = 0.5f }, + ], cancellationToken); + Console.WriteLine($"[TEST 2] sum of 2 complex number is: {result3}"); // test 4: call IPC service method without parameter or return - await systemClient.DoNothing(cancellationToken); + await systemClient.FireAndForgetWithCt(cancellationToken); Console.WriteLine($"[TEST 4] invoked DoNothing()"); //((IDisposable)systemClient).Dispose(); // test 5: call IPC service method with enum parameter - string text = await systemClient.ConvertText("hEllO woRd!", TextStyle.Upper, cancellationToken); + var text = await systemClient.DanishNameOfDay(DayOfWeek.Sunday, cancellationToken); Console.WriteLine($"[TEST 5] {text}"); // test 6: call IPC service method returning GUID - Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), cancellationToken); + Guid generatedId = await systemClient.EchoGuidAfter(Guid.NewGuid(), waitOnServer: default, ct: cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array - byte[] input = Encoding.UTF8.GetBytes(string.Concat(Enumerable.Range(1, 1).Select(_ => "Test"))); + byte[] input = Encoding.UTF8.GetBytes("Test"); byte[] reversed = await systemClient.ReverseBytes(input, cancellationToken); Console.WriteLine($"[TEST 7] reverse bytes"); // test 8: call IPC service method with callback - var userName = await computingClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + var userName = await computingClient.SendMessage(ct: cancellationToken); Console.WriteLine($"[TEST 8] client identity : {userName}"); //// test 9: call IPC service method with message parameter @@ -109,7 +112,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) { stopwatch.Stop(); Console.WriteLine(); - Console.WriteLine("Calls per second: " + count*8 / stopwatch.Elapsed.TotalSeconds); + Console.WriteLine("Calls per second: " + count * 8 / stopwatch.Elapsed.TotalSeconds); Console.WriteLine(); } // test 10: call slow IPC service method @@ -119,6 +122,6 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj index a313c304..661d8ca9 100644 --- a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj +++ b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj @@ -2,10 +2,11 @@ Exe - net7.0;net461;net7.0-windows + net6.0;net461;net6.0-windows app1.manifest latest true + enable @@ -14,9 +15,9 @@ - - - - + + + + diff --git a/src/IpcSample.ConsoleClient/TcpClient.cs b/src/IpcSample.ConsoleClient/TcpClient.cs index 0768ebc6..05a56a97 100644 --- a/src/IpcSample.ConsoleClient/TcpClient.cs +++ b/src/IpcSample.ConsoleClient/TcpClient.cs @@ -1,15 +1,16 @@ using System.Text; using System.Diagnostics; -using UiPath.CoreIpc.Tcp; using Microsoft.Extensions.DependencyInjection; using System.Net; +using UiPath.Ipc.Transport.Tcp; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -class TcpClient +internal static class TcpClient { - static readonly IPEndPoint SystemEndPoint = new(IPAddress.Loopback, 3131); - static async Task _Main(string[] args) + private static readonly IPEndPoint SystemEndPoint = new(IPAddress.Loopback, 3131); + + public static async Task _Main(string[] args) { Console.WriteLine(typeof(int).Assembly); Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); @@ -33,46 +34,49 @@ await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => private static async Task RunTestsAsync(CancellationToken cancellationToken) { var serviceProvider = ConfigureServices(); - var callback = new ComputingCallback { Id = "custom made" }; - var computingClientBuilder = new TcpClientBuilder(SystemEndPoint, serviceProvider) - .SerializeParametersAsObjects().CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); + var callback = new ComputingCallback(); + var ipcClient = new IpcClient + { + Transport = new TcpClientTransport { EndPoint = SystemEndPoint }, + Callbacks = new() + { + { typeof(IComputingCallback), callback } + }, + ServiceProvider = serviceProvider, + RequestTimeout = TimeSpan.FromSeconds(2), + }; + var stopwatch = Stopwatch.StartNew(); int count = 0; try { - var computingClient = computingClientBuilder.ValidateAndBuild(); - var systemClient = - new TcpClientBuilder(SystemEndPoint) - .SerializeParametersAsObjects() - //.EncryptAndSign("localhost") - .RequestTimeout(TimeSpan.FromSeconds(2)) - .Logger(serviceProvider) - .ValidateAndBuild(); + var computingClient = ipcClient.GetProxy(); + var systemClient = ipcClient.GetProxy(); var watch = Stopwatch.StartNew(); //using (var file = File.OpenRead(@"C:\Windows\DPINST.log")) //{ // Console.WriteLine(await systemClient.Upload(file)); //} - for (int i =0; i<50;i++) + for (int i = 0; i < 50; i++) { // test 1: call IPC service method with primitive types - float result1 = await computingClient.AddFloat(1.23f, 4.56f, cancellationToken); + float result1 = await computingClient.AddFloats(1.23f, 4.56f, cancellationToken); count++; Console.WriteLine($"[TEST 1] sum of 2 floating number is: {result1}"); // test 2: call IPC service method with complex types - ComplexNumber result2 = await computingClient.AddComplexNumber( - new ComplexNumber(0.1f, 0.3f), - new ComplexNumber(0.2f, 0.6f), cancellationToken); - Console.WriteLine($"[TEST 2] sum of 2 complexe number is: {result2.A}+{result2.B}i"); + ComplexNumber result2 = await computingClient.AddComplexNumbers( + new ComplexNumber { I = 0.1f, J = 0.3f }, + new ComplexNumber { I = 0.2f, J = 0.6f }, cancellationToken); + Console.WriteLine($"[TEST 2] sum of 2 complexe number is: {result2}"); // test 3: call IPC service method with an array of complex types - ComplexNumber result3 = await computingClient.AddComplexNumbers(new[] - { - new ComplexNumber(0.5f, 0.4f), - new ComplexNumber(0.2f, 0.1f), - new ComplexNumber(0.3f, 0.5f), - }, cancellationToken); - Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3.A}+{result3.B}i", cancellationToken); + ComplexNumber result3 = await computingClient.AddComplexNumberList( + [ + new ComplexNumber{ I = 0.5f, J = 0.4f }, + new ComplexNumber{ I = 0.2f, J = 0.1f }, + new ComplexNumber{ I = 0.3f, J = 0.5f }, + ], cancellationToken); + Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3}", cancellationToken); // test 4: call IPC service method without parameter or return //await systemClient.DoNothing(cancellationToken); @@ -80,11 +84,11 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) //((IDisposable)systemClient).Dispose(); // test 5: call IPC service method with enum parameter - string text = await systemClient.ConvertText("hEllO woRd!", TextStyle.Upper, cancellationToken); + string text = await systemClient.DanishNameOfDay(DayOfWeek.Sunday, cancellationToken); Console.WriteLine($"[TEST 5] {text}"); // test 6: call IPC service method returning GUID - Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), cancellationToken); + Guid generatedId = await systemClient.EchoGuidAfter(Guid.NewGuid(), waitOnServer: default, ct: cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array @@ -93,7 +97,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) Console.WriteLine($"[TEST 7] reverse bytes"); // test 8: call IPC service method with callback - var userName = await computingClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + var userName = await computingClient.SendMessage(ct: cancellationToken); Console.WriteLine($"[TEST 8] client identity : {userName}"); //// test 9: call IPC service method with message parameter @@ -114,8 +118,8 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) callbackProxy.Dispose(); callbackProxy.Dispose(); //((IpcProxy)callbackProxy).CloseConnection(); - ((IpcProxy)computingClient).CloseConnection(); - ((IpcProxy)systemClient).CloseConnection(); + await ((IpcProxy)computingClient).CloseConnection(); + await ((IpcProxy)systemClient).CloseConnection(); } finally { @@ -131,6 +135,6 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleClient/WebSocketClient.cs b/src/IpcSample.ConsoleClient/WebSocketClient.cs index e6c93422..394c469e 100644 --- a/src/IpcSample.ConsoleClient/WebSocketClient.cs +++ b/src/IpcSample.ConsoleClient/WebSocketClient.cs @@ -1,11 +1,13 @@ -using System.Text; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.WebSockets; -using Microsoft.Extensions.DependencyInjection; -namespace UiPath.CoreIpc.Tests; -class WebSocketClient +using System.Text; +using UiPath.Ipc.Transport.WebSocket; + +namespace UiPath.Ipc.Tests; + +internal static class WebSocketClient { - static async Task _Main(string[] args) + public static async Task _Main(string[] args) { Console.WriteLine(typeof(int).Assembly); Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); @@ -31,45 +33,48 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) { Uri uri = new("ws://localhost:1212/wsDemo/"); var serviceProvider = ConfigureServices(); - var callback = new ComputingCallback { Id = "custom made" }; - var computingClientBuilder = new WebSocketClientBuilder(uri, serviceProvider).SerializeParametersAsObjects() - .CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); + var callback = new ComputingCallback(); + var ipcClient = new IpcClient + { + Transport = new WebSocketClientTransport { Uri = uri }, + Callbacks = new() + { + { typeof(IComputingCallback), callback } + }, + ServiceProvider = serviceProvider, + RequestTimeout = TimeSpan.FromSeconds(2) + }; var stopwatch = Stopwatch.StartNew(); int count = 0; try { - var computingClient = computingClientBuilder.ValidateAndBuild(); - var systemClient = - new WebSocketClientBuilder(uri).SerializeParametersAsObjects() - //.EncryptAndSign("localhost") - .RequestTimeout(TimeSpan.FromSeconds(2)) - .Logger(serviceProvider) - .ValidateAndBuild(); + var computingClient = ipcClient.GetProxy();; + var systemClient = ipcClient.GetProxy(); var watch = Stopwatch.StartNew(); //using (var file = File.OpenRead(@"C:\Windows\DPINST.log")) //{ // Console.WriteLine(await systemClient.Upload(file)); //} - for (int i =0; i<50;i++) + for (int i = 0; i < 50; i++) { // test 1: call IPC service method with primitive types - float result1 = await computingClient.AddFloat(1.23f, 4.56f, cancellationToken); + float result1 = await computingClient.AddFloats(1.23f, 4.56f, cancellationToken); count++; Console.WriteLine($"[TEST 1] sum of 2 floating number is: {result1}"); // test 2: call IPC service method with complex types - ComplexNumber result2 = await computingClient.AddComplexNumber( - new ComplexNumber(0.1f, 0.3f), - new ComplexNumber(0.2f, 0.6f), cancellationToken); - Console.WriteLine($"[TEST 2] sum of 2 complexe number is: {result2.A}+{result2.B}i"); + ComplexNumber result2 = await computingClient.AddComplexNumbers( + new ComplexNumber { I = 0.1f, J = 0.3f }, + new ComplexNumber{ I = 0.2f, J = 0.6f }, cancellationToken); + Console.WriteLine($"[TEST 2] sum of 2 complexe number is: {result2}"); // test 3: call IPC service method with an array of complex types - ComplexNumber result3 = await computingClient.AddComplexNumbers(new[] - { - new ComplexNumber(0.5f, 0.4f), - new ComplexNumber(0.2f, 0.1f), - new ComplexNumber(0.3f, 0.5f), - }, cancellationToken); - Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3.A}+{result3.B}i", cancellationToken); + ComplexNumber result3 = await computingClient.AddComplexNumberList( + [ + new ComplexNumber{ I = 0.5f, J = 0.4f }, + new ComplexNumber{ I = 0.2f, J = 0.1f }, + new ComplexNumber{ I = 0.3f, J = 0.5f }, + ], cancellationToken); + Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3}", cancellationToken); // test 4: call IPC service method without parameter or return //await systemClient.DoNothing(cancellationToken); @@ -77,11 +82,11 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) //((IDisposable)systemClient).Dispose(); // test 5: call IPC service method with enum parameter - string text = await systemClient.ConvertText("hEllO woRd!", TextStyle.Upper, cancellationToken); + string text = await systemClient.DanishNameOfDay(DayOfWeek.Sunday, cancellationToken); Console.WriteLine($"[TEST 5] {text}"); // test 6: call IPC service method returning GUID - Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), cancellationToken); + Guid generatedId = await systemClient.EchoGuidAfter(Guid.NewGuid(), waitOnServer: default, ct: cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array @@ -90,7 +95,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) Console.WriteLine($"[TEST 7] reverse bytes"); // test 8: call IPC service method with callback - var userName = await computingClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + var userName = await computingClient.SendMessage(ct: cancellationToken); Console.WriteLine($"[TEST 8] client identity : {userName}"); //// test 9: call IPC service method with message parameter @@ -111,8 +116,8 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) callbackProxy.Dispose(); callbackProxy.Dispose(); //((IpcProxy)callbackProxy).CloseConnection(); - ((IpcProxy)computingClient).CloseConnection(); - ((IpcProxy)systemClient).CloseConnection(); + await ((IpcProxy)computingClient).CloseConnection(); + await ((IpcProxy)systemClient).CloseConnection(); } finally { @@ -128,6 +133,6 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj index 2c9744b6..bd1e52fa 100644 --- a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj +++ b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj @@ -2,10 +2,11 @@ Exe - net7.0;net461;net7.0-windows + net6.0;net461;net6.0-windows app1.manifest latest true + enable @@ -14,10 +15,10 @@ - - - - + + + + diff --git a/src/IpcSample.ConsoleServer/Server.cs b/src/IpcSample.ConsoleServer/Server.cs index 8e9ad9e4..3f77031b 100644 --- a/src/IpcSample.ConsoleServer/Server.cs +++ b/src/IpcSample.ConsoleServer/Server.cs @@ -1,48 +1,51 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.NamedPipe; +using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -class Server +internal static class Server { - //private static readonly Timer _timer = new Timer(_ => - //{ - // Console.WriteLine("GC.Collect"); - // GC.Collect(); - // GC.WaitForPendingFinalizers(); - // GC.Collect(); - //}, null, 0, 3000); - static async Task Main() + public static async Task Main() { Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); //GuiLikeSyncContext.Install(); Console.WriteLine(SynchronizationContext.Current); var serviceProvider = ConfigureServices(); // build and run service host - var host = new ServiceHostBuilder(serviceProvider) - .UseNamedPipes(new NamedPipeSettings("test") + + await using var ipcServer = new IpcServer + { + Transport = new NamedPipeServerTransport { PipeName = "test" }, + ServiceProvider = serviceProvider, + Endpoints = new() { - RequestTimeout = TimeSpan.FromSeconds(2), - //AccessControl = security => security.AllowCurrentUser(), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); + typeof(IComputingService), + typeof(ISystemService) + }, + RequestTimeout = TimeSpan.FromSeconds(2), + }; + + Console.WriteLine(typeof(int).Assembly); + ipcServer.Start(); + await ipcServer.WaitForStart(); + Console.WriteLine("Server started."); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + var tcs = new TaskCompletionSource(); + Console.CancelKeyPress += (_, e) => { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - host.Dispose(); - })); + e.Cancel = true; + tcs.TrySetResult(null); + }; + await tcs.Task; + await ipcServer.DisposeAsync(); Console.WriteLine("Server stopped."); } private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .AddSingleton() .AddSingleton() .BuildServiceProvider(); diff --git a/src/IpcSample.ConsoleServer/TcpServer.cs b/src/IpcSample.ConsoleServer/TcpServer.cs index 172a7725..88b3a9b9 100644 --- a/src/IpcSample.ConsoleServer/TcpServer.cs +++ b/src/IpcSample.ConsoleServer/TcpServer.cs @@ -1,22 +1,17 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using System.Net; -using UiPath.CoreIpc.Tcp; +using UiPath.Ipc.Transport.Tcp; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -class TcpServer +using IPEndPoint = System.Net.IPEndPoint; +using IPAddress = System.Net.IPAddress; + +internal static class TcpServer { - static readonly IPEndPoint SystemEndPoint = new(IPAddress.Any, 3131); - //private static readonly Timer _timer = new Timer(_ => - //{ - // Console.WriteLine("GC.Collect"); - // GC.Collect(); - // GC.WaitForPendingFinalizers(); - // GC.Collect(); - //}, null, 0, 3000); - - static async Task _Main() + private static readonly IPEndPoint SystemEndPoint = new(IPAddress.Any, 3131); + + public static async Task _Main() { Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); //GuiLikeSyncContext.Install(); @@ -24,29 +19,38 @@ static async Task _Main() var serviceProvider = ConfigureServices(); // build and run service host var data = File.ReadAllBytes(@"../../../../localhost.pfx"); - var host = new ServiceHostBuilder(serviceProvider) - .UseTcp(new TcpSettings(SystemEndPoint) + + await using var ipcServer = new IpcServer + { + Transport = new TcpServerTransport { EndPoint = SystemEndPoint }, + ServiceProvider = serviceProvider, + Endpoints = new() { - RequestTimeout = TimeSpan.FromSeconds(2), - //Certificate = new X509Certificate(data, "1"), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + typeof(IComputingService), + typeof(ISystemService) + }, + RequestTimeout = TimeSpan.FromSeconds(2), + }; + ipcServer.Start(); + await ipcServer.WaitForStart(); + + Console.WriteLine("Server started."); + + var tcs = new TaskCompletionSource(); + Console.CancelKeyPress += (_, e) => { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - host.Dispose(); - })); + e.Cancel = true; + tcs.TrySetResult(null); + }; + await tcs.Task; + await ipcServer.DisposeAsync(); Console.WriteLine("Server stopped."); } private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .AddSingleton() .AddSingleton() .BuildServiceProvider(); diff --git a/src/IpcSample.ConsoleServer/WebSocketServer.cs b/src/IpcSample.ConsoleServer/WebSocketServer.cs index 322c1d65..60244ed0 100644 --- a/src/IpcSample.ConsoleServer/WebSocketServer.cs +++ b/src/IpcSample.ConsoleServer/WebSocketServer.cs @@ -1,20 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using System.Net; -using System.Net.WebSockets; -using UiPath.CoreIpc.WebSockets; -namespace UiPath.CoreIpc.Tests; +using UiPath.Ipc.Transport.WebSocket; +namespace UiPath.Ipc.Tests; class WebSocketServer { - //private static readonly Timer _timer = new Timer(_ => - //{ - // Console.WriteLine("GC.Collect"); - // GC.Collect(); - // GC.WaitForPendingFinalizers(); - // GC.Collect(); - //}, null, 0, 3000); - - static async Task _Main() + public static async Task _Main() { Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); //GuiLikeSyncContext.Install(); @@ -22,27 +12,44 @@ static async Task _Main() var serviceProvider = ConfigureServices(); // build and run service host //var data = File.ReadAllBytes(@"../../../../localhost.pfx"); - var host = new ServiceHostBuilder(serviceProvider) - .UseWebSockets(new(new HttpSysWebSocketsListener("http://localhost:1212/wsDemo/").Accept) + + await using var ipcServer = new IpcServer + { + Transport = new WebSocketServerTransport + { + Accept = new HttpSysWebSocketsListener("http://localhost:1212/wsDemo/").Accept, + }, + ServiceProvider = serviceProvider, + Endpoints = new() { - RequestTimeout = TimeSpan.FromSeconds(2), - //Certificate = new X509Certificate(data, "1"), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + typeof(IComputingService), + typeof(ISystemService) + }, + RequestTimeout = TimeSpan.FromSeconds(2), + }; + + Console.WriteLine(typeof(int).Assembly); + + ipcServer.Start(); + await ipcServer.WaitForStart(); + Console.WriteLine("Server started."); + + // console cancellationtoken + var tcs = new TaskCompletionSource(); + Console.CancelKeyPress += (_, e) => { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - host.Dispose(); - })); + e.Cancel = true; + tcs.TrySetResult(null); + }; + await tcs.Task; + await ipcServer.DisposeAsync(); + Console.WriteLine("Server stopped."); return; } private static IServiceProvider ConfigureServices() => new ServiceCollection() - .AddIpcWithLogging() + .AddLogging() .AddSingleton() .AddSingleton() .BuildServiceProvider(); diff --git a/src/Playground/Contracts.cs b/src/Playground/Contracts.cs new file mode 100644 index 00000000..dd847ede --- /dev/null +++ b/src/Playground/Contracts.cs @@ -0,0 +1,24 @@ +using UiPath.Ipc; + +namespace Playground; + +public static class Contracts +{ + public const string PipeName = "SomePipe"; + + public interface IServerOperations + { + Task Register(Message? m = null); + Task Broadcast(string text); + } + + public interface IClientOperations + { + Task Greet(string text); + } + + public interface IClientOperations2 + { + Task GetTheTime(); + } +} diff --git a/src/Playground/Impl.cs b/src/Playground/Impl.cs new file mode 100644 index 00000000..66bc292d --- /dev/null +++ b/src/Playground/Impl.cs @@ -0,0 +1,68 @@ +using System.Collections.Concurrent; +using UiPath.Ipc; + +namespace Playground; + +internal static class Impl +{ + public sealed class ClientRegistry + { + private readonly ConcurrentDictionary _clients = new(); + + public bool Add(ClientPair pair) => _clients.TryAdd(pair, value: null); + + public IReadOnlyList All() => _clients.Keys.ToArray(); + } + + public readonly record struct ClientPair(Contracts.IClientOperations Client, Contracts.IClientOperations2 Client2); + + public sealed class Server(ClientRegistry clients) : Contracts.IServerOperations + { + public async Task Register(Message? m = null) + { + var clientOps = m!.Client.GetCallback(); + var clientOps2 = m.Client.GetCallback(); + + var added = clients.Add(new(clientOps, clientOps2)); + + if (added) + { + Console.WriteLine("New client registered."); + } + else + { + Console.WriteLine("Client tried to register again resulting in a NOP."); + } + + return true; + } + + public async Task Broadcast(string text) + { + var pairs = clients.All(); + + foreach (var pair in pairs) + { + var time = await pair.Client2.GetTheTime(); + _ = await pair.Client.Greet($"{text} - You said the time was: {time}"); + } + + return true; + } + } + + public sealed class ClientOperations() : Contracts.IClientOperations + { + public async Task Greet(string text) + { + Console.WriteLine($"Scheduler: {TaskScheduler.Current.GetType().Name}"); + Console.WriteLine($"Server says: {text}"); + return true; + } + } + + public sealed class Client2 : Contracts.IClientOperations2 + { + public Task GetTheTime() => Task.FromResult(DateTime.Now); + } +} diff --git a/src/Playground/Playground.csproj b/src/Playground/Playground.csproj new file mode 100644 index 00000000..e6a9a7fb --- /dev/null +++ b/src/Playground/Playground.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + 1998 + + + + + + + + + + + + + + diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs new file mode 100644 index 00000000..45d6453a --- /dev/null +++ b/src/Playground/Program.cs @@ -0,0 +1,135 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Playground; +using UiPath.Ipc; +using UiPath.Ipc.Transport.NamedPipe; + +internal class Program +{ + private static async Task Main(string[] args) + { + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + Uri serverUri = new("http://localhost:62234"); + Uri clientUri = new("http://localhost:62235"); + + var cancelled = Task.Delay(Timeout.InfiniteTimeSpan, cts.Token); + + var serverScheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler; + var clientScheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler; + + await using var serverSP = new ServiceCollection() + .AddSingleton() + .AddScoped() + .AddLogging(builder => builder.AddConsole()) + .BuildServiceProvider(); + + await using var clientSP = new ServiceCollection() + .AddScoped() + .AddLogging(builder => builder.AddConsole()) + .BuildServiceProvider(); + + await using var ipcServer = new IpcServer() + { + Scheduler = serverScheduler, + ServiceProvider = serverSP, + RequestTimeout = TimeSpan.FromHours(10), + Endpoints = new() + { + typeof(Contracts.IServerOperations), // DEVINE + new ContractSettings(typeof(Contracts.IServerOperations)) // ASTALALT + { + BeforeIncomingCall = async (callInfo, _) => + { + Console.WriteLine($"Server: {callInfo.Method.Name}"); + } + }, + typeof(Contracts.IClientOperations2) + }, + Transport = new NamedPipeServerTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AccessControl = ps => + { + }, + MaxReceivedMessageSizeInMegabytes = 100, + } + }; + + try + { + ipcServer.Start(); // ar putea fi void, ar putea fi si Run + // await ipcServer.WaitForStart(); + } + catch (Exception ex) + { + Console.WriteLine("Failed to start."); + Console.WriteLine(ex.ToString()); + throw; + } + + var c1 = new IpcClient() + { + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + ServiceProvider = clientSP, + Scheduler = clientScheduler, + Transport = new NamedPipeClientTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AllowImpersonation = false, + }, + DebugName = "Client1", + }; + + var c2 = new IpcClient() + { + ServiceProvider = clientSP, + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + Scheduler = clientScheduler, + Transport = new NamedPipeClientTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AllowImpersonation = false, + }, + }; + + var proxy1 = new IpcClient() + { + ServiceProvider = clientSP, + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + Scheduler = clientScheduler, + Transport = new NamedPipeClientTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AllowImpersonation = false, + }, + }.GetProxy(); + + + await proxy1.Register(); + await proxy1.Broadcast("Hello Bidirectional Http!"); + + await Task.WhenAny(cancelled); + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs new file mode 100644 index 00000000..4d522c11 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs @@ -0,0 +1,24 @@ +namespace UiPath.Ipc.Extensions.Abstractions; + +public abstract class ServerTransportBase : ServerTransport +{ + protected abstract ServerState CreateState(); + protected new abstract IEnumerable Validate(); + + internal override IServerState CreateServerState() => CreateState(); + internal override IEnumerable ValidateCore() => Validate(); +} +public abstract class ServerState : ServerTransport.IServerState +{ + public abstract ValueTask DisposeAsync(); + public abstract ServerConnectionSlot CreateServerConnectionSlot(); + + ServerTransport.IServerConnectionSlot ServerTransport.IServerState.CreateConnectionSlot() => CreateServerConnectionSlot(); +} + +public abstract class ServerConnectionSlot : ServerTransport.IServerConnectionSlot +{ + public abstract ValueTask AwaitConnection(CancellationToken ct); + + public abstract ValueTask DisposeAsync(); +} diff --git a/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj new file mode 100644 index 00000000..4c768d09 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net461;net6.0-windows + enable + enable + latest + true + enable + UiPath.Ipc.Extensions.Abstractions + UiPath.Ipc.Extensions.Abstractions + + + + + + + diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs new file mode 100644 index 00000000..190b0ceb --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs @@ -0,0 +1,284 @@ +using Nito.AsyncEx; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Net; +using System.Net.Http; +using System.Threading.Channels; + +namespace UiPath.Ipc.Extensions.BidirectionalHttp; + +using static Constants; + +public sealed partial class BidiHttpServerTransport : ServerTransportBase +{ + public required Uri Uri { get; set; } + + protected override ServerState CreateState() + => new BidiHttpServerState(this); + + protected override IEnumerable Validate() => []; + + private sealed class BidiHttpServerState : ServerState + { + private readonly CancellationTokenSource _cts = new(); + private readonly HttpListener _httpListener; + private readonly Task _processing; + private readonly Lazy _disposing; + + private readonly ConcurrentDictionary> _connections = new(); + private readonly Channel<(Guid connectionId, Uri reverseUri)> _newConnections = Channel.CreateUnbounded<(Guid connectionId, Uri reverseUri)>(); + + public ChannelReader<(Guid connectionId, Uri reverseUri)> NewConnections => _newConnections.Reader; + public ChannelReader GetConnectionChannel(Guid connectionId) => _connections[connectionId]; + + public BidiHttpServerState(BidiHttpServerTransport transport) + { + _httpListener = new HttpListener() + { + Prefixes = + { + transport.Uri.ToString() + } + }; + _processing = ProcessContexts(); + _disposing = new(DisposeCore); + } + + public override ValueTask DisposeAsync() => new(_disposing.Value); + + private async Task DisposeCore() + { + _cts.Cancel(); + try + { + await _processing; + } + catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token) + { + } + + foreach (var pair in _connections) + { + pair.Value.Writer.Complete(); + } + _cts.Dispose(); + } + + private async Task ProcessContexts() + { + await foreach (var (context, connectionId, reverseUri) in AwaitContexts()) + { + var connectionChannel = _connections.GetOrAdd(connectionId, _ => + { + _newConnections.Writer.TryWrite((connectionId, reverseUri)); + return Channel.CreateUnbounded(); + }); + + await connectionChannel.Writer.WriteAsync(context, _cts.Token); + } + + async IAsyncEnumerable<(HttpListenerContext context, Guid connectionId, Uri reverseUri)> AwaitContexts() + { + while (!_cts.Token.IsCancellationRequested) + { + var context = await _httpListener.GetContextAsync(); + + if (!TryAcceptContext(context, out var connectionId, out var reverseUri)) + { + context.Response.StatusCode = 400; + context.Response.Close(); + continue; + } + + yield return (context, connectionId, reverseUri); + } + } + + bool TryAcceptContext(HttpListenerContext context, out Guid connectionId, [NotNullWhen(returnValue: true)] out Uri? reverseUri) + { + if (!Guid.TryParse(context.Request.Headers[ConnectionIdHeader], out connectionId) || + !Uri.TryCreate(context.Request.Headers[ReverseUriHeader], UriKind.Absolute, out reverseUri)) + { + connectionId = Guid.Empty; + reverseUri = null; + return false; + } + + return true; + } + } + + public override ServerConnectionSlot CreateServerConnectionSlot() => new BidiHttpServerConnectionSlot(this); + + } + + private sealed class BidiHttpServerConnectionSlot : ServerConnectionSlot + { + private readonly Pipe _pipe = new(); + private readonly BidiHttpServerState _serverState; + private readonly Lazy _disposing; + private readonly CancellationTokenSource _cts = new(); + private readonly AsyncLock _lock = new(); + private (Guid connectionId, Uri reverseUri)? _connection = null; + private HttpClient? _client; + private Task? _processing = null; + + public BidiHttpServerConnectionSlot(BidiHttpServerState serverState) + { + _serverState = serverState; + _disposing = new(DisposeCore); + } + + private async Task DisposeCore() + { + _cts.Cancel(); + + _client?.Dispose(); + + try + { + await (_processing ?? Task.CompletedTask); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token) + { + // ignored + } + + _cts.Dispose(); + } + + public override async ValueTask AwaitConnection(CancellationToken ct) + { + using (await _lock.LockAsync(ct)) + { + if (_connection is not null) + { + throw new InvalidOperationException(); + } + + _connection = await _serverState.NewConnections.ReadAsync(ct); + + _client = new() + { + BaseAddress = _connection.Value.reverseUri, + DefaultRequestHeaders = + { + { ConnectionIdHeader, _connection.Value.connectionId.ToString() } + } + }; + + _processing = ProcessContexts(_cts.Token); + + return new Adapter(this); + } + } + + public override ValueTask DisposeAsync() => new(_disposing.Value); + + private async Task ProcessContexts(CancellationToken ct) + { + var reader = _serverState.GetConnectionChannel(_connection!.Value.connectionId); + + while (await reader.WaitToReadAsync(ct)) + { + if (!reader.TryRead(out var context)) + { + continue; + } + await ProcessContext(context); + } + + async Task ProcessContext(HttpListenerContext context) + { + try + { + while (true) + { + var memory = _pipe.Writer.GetMemory(); + var cbRead = await context.Request.InputStream.ReadAsync(memory, ct); + if (cbRead is 0) + { + break; + } + _pipe.Writer.Advance(cbRead); + var flushResult = await _pipe.Writer.FlushAsync(ct); + if (flushResult.IsCompleted) + { + break; + } + } + } + finally + { + context.Response.StatusCode = 200; + context.Response.Close(); + } + } + } + + private sealed class Adapter : Stream + { + private readonly BidiHttpServerConnectionSlot _slot; + + public Adapter(BidiHttpServerConnectionSlot slot) + { + _slot = slot; + } + + public +#if !NET461 + override +#endif + ValueTask DisposeAsync() => _slot.DisposeAsync(); + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) + { + var memory = new Memory(buffer, offset, count); + var readResult = await _slot._pipe.Reader.ReadAsync(ct); + + var take = (int)Math.Min(readResult.Buffer.Length, memory.Length); + + readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); + _slot._pipe.Reader.AdvanceTo(readResult.Buffer.GetPosition(take)); + + return take; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + { + var memory = new ReadOnlyMemory(buffer, offset, count); + if (_slot._client is null) + { + throw new InvalidOperationException(); + } + + HttpContent content = +#if NET461 + new ByteArrayContent(memory.ToArray()); +#else + new ReadOnlyMemoryContent(memory); +#endif + + await _slot._client.PostAsync(requestUri: "", content, ct); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public override void Flush() => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + } + } +} diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs new file mode 100644 index 00000000..e24c8211 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs @@ -0,0 +1,8 @@ +namespace UiPath.Ipc.Extensions.BidirectionalHttp; + +internal static class Constants +{ + internal const string ConnectionIdHeader = "X-UiPathIpc-ConnectionId"; + internal const string ReverseUriHeader = "X-UiPathIpc-ReverseUri"; +} + diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs new file mode 100644 index 00000000..403f1379 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs @@ -0,0 +1 @@ +global using UiPath.Ipc.Extensions.Abstractions; diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..45ded093 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,35 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +// +// Summary: +// Allows capturing of the expressions passed to a method. +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + // + // Summary: + // Initializes a new instance of the System.Runtime.CompilerServices.CallerArgumentExpressionAttribute + // class. + // + // Parameters: + // parameterName: + // The name of the targeted parameter. + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + // + // Summary: + // Gets the target parameter name of the CallerArgumentExpression. + // + // Returns: + // The name of the targeted parameter of the CallerArgumentExpression. + public string ParameterName { get; } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CancellationTokenExtensions.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CancellationTokenExtensions.cs new file mode 100644 index 00000000..95b0cde0 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CancellationTokenExtensions.cs @@ -0,0 +1,11 @@ +#if NET461 + +namespace System.Threading; + +internal static class CancellationTokenExtensions +{ + public static CancellationTokenRegistration UnsafeRegister(this CancellationToken token, Action callback, object? state) + => token.Register(callback, state); +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CompilerFeatureRequiredAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 00000000..c40e1129 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,56 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +/// +/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: AttributeTargets.All, + AllowMultiple = true, + Inherited = false)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.compilerfeaturerequiredattribute")] +#if PolyPublic +public +#endif +sealed class CompilerFeatureRequiredAttribute : + Attribute +{ + /// + /// Initialize a new instance of + /// + /// The name of the required compiler feature. + public CompilerFeatureRequiredAttribute(string featureName) => + FeatureName = featureName; + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof(RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof(RequiredMembers); +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/EnumerableExtensions.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/EnumerableExtensions.cs new file mode 100644 index 00000000..dba5280f --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +#if NET461 + +namespace System.Linq; + +internal static class EnumerableExtensions +{ + public static IEnumerable Prepend(this IEnumerable enumerable, T element) + { + yield return element; + foreach (var item in enumerable) + { + yield return item; + } + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/IsExternalInit.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/IsExternalInit.cs new file mode 100644 index 00000000..dc521fbf --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/IsExternalInit.cs @@ -0,0 +1,19 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +/// +/// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal static class IsExternalInit +{ +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullAttribute.cs new file mode 100644 index 00000000..bf9ad270 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullAttribute.cs @@ -0,0 +1,71 @@ +// +#pragma warning disable + +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX + +namespace System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +/// +/// Specifies that the method or property will ensure that the listed field and property members have +/// non- values when returning with the specified return value condition. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Method | + Targets.Property, + Inherited = false, + AllowMultiple = true)] +#if PolyPublic +public +#endif +sealed class MemberNotNullWhenAttribute : + Attribute +{ + /// + /// Gets the return value condition. + /// + public bool ReturnValue { get; } + + /// + /// Gets field or property member names. + /// + public string[] Members { get; } + + /// + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// + /// The field or property member that is promised to be not-. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// + /// Initializes the attribute with the specified return value condition and list + /// of field and property members. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullWhenAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullWhenAttribute.cs new file mode 100644 index 00000000..b49daaab --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullWhenAttribute.cs @@ -0,0 +1,38 @@ +// +#pragma warning disable + +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX + +namespace System.Diagnostics.CodeAnalysis; + +// +// Summary: +// Specifies that the method or property will ensure that the listed field and property +// members have values that aren't null. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +internal sealed class MemberNotNullAttribute : Attribute +{ + // + // Summary: + // Initializes the attribute with a field or property member. + // + // Parameters: + // member: + // The field or property member that is promised to be non-null. + public MemberNotNullAttribute(string member) : this([member]) { } + // + // Summary: + // Initializes the attribute with the list of field and property members. + // + // Parameters: + // members: + // The list of field and property members that are promised to be non-null. + public MemberNotNullAttribute(params string[] members) => Members = members; + + // + // Summary: + // Gets field or property member names. + public string[] Members { get; } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullIfNotNullAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullIfNotNullAttribute.cs new file mode 100644 index 00000000..50afab43 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullIfNotNullAttribute.cs @@ -0,0 +1,41 @@ +// +#pragma warning disable + +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X + +namespace System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Parameter | + Targets.Property | + Targets.ReturnValue, + AllowMultiple = true)] +#if PolyPublic +public +#endif +sealed class NotNullIfNotNullAttribute : + Attribute +{ + /// + /// Gets the associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public string ParameterName { get; } + + /// + /// Initializes the attribute with the associated parameter name. + /// + /// + /// The associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public NotNullIfNotNullAttribute(string parameterName) => + ParameterName = parameterName; +} +#endif diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullWhenAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullWhenAttribute.cs new file mode 100644 index 00000000..be043ad9 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullWhenAttribute.cs @@ -0,0 +1,38 @@ +// +#pragma warning disable + +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that when a method returns , +/// the parameter will not be even if the corresponding type allows it. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Parameter)] +#if PolyPublic +public +#endif +sealed class NotNullWhenAttribute : + Attribute +{ + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public NotNullWhenAttribute(bool returnValue) => + ReturnValue = returnValue; +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/RequiredMemberAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/RequiredMemberAttribute.cs new file mode 100644 index 00000000..8564df41 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/RequiredMemberAttribute.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +/// +/// Specifies that a type has required members or that a member is required. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Class | + Targets.Struct | + Targets.Field | + Targets.Property, + Inherited = false)] +#if PolyPublic +public +#endif +sealed class RequiredMemberAttribute : + Attribute; +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/SetsRequiredMembersAttribute.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/SetsRequiredMembersAttribute.cs new file mode 100644 index 00000000..f3db6439 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/SetsRequiredMembersAttribute.cs @@ -0,0 +1,20 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that this constructor sets all required members for the current type, and callers +/// do not need to set any required members themselves. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Constructor)] +#if PolyPublic +public +#endif +sealed class SetsRequiredMembersAttribute : + Attribute; +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/StreamExtensions.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/StreamExtensions.cs new file mode 100644 index 00000000..4c23ae4e --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/StreamExtensions.cs @@ -0,0 +1,43 @@ +#pragma warning disable + +#if (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +internal static class StreamExtensions +{ + /// + /// Asynchronously reads a sequence of bytes from the current stream, advances the position within the stream by + /// the number of bytes read, and monitors cancellation requests. + /// + /// The region of memory to write the data into. + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// + /// A task that represents the asynchronous read operation. The value of its Result property contains the + /// total number of bytes read into the buffer. The result value can be less than the number of bytes allocated in + /// the buffer if that many bytes are not currently available, or it can be 0 (zero) if the end of the stream has + /// been reached. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.readasync#system-io-stream-readasync(system-memory((system-byte))-system-threading-cancellationtoken)")] + public static ValueTask ReadAsync( + this Stream target, + Memory buffer, + CancellationToken cancellationToken = default) + { + if (!MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } +} + +#endif diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/System_Index.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/System_Index.cs new file mode 100644 index 00000000..177b155c --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/System_Index.cs @@ -0,0 +1,151 @@ +// +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal readonly struct Index : IEquatable +{ + readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (fromEnd) + { + _value = ~value; + } + else + { + _value = value; + } + } + + // The following private constructors mainly created for perf reason to avoid the checks + Index(int value) => + _value = value; + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object value) => value is Index index && _value == index._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + { + return ToStringFromEnd(); + } + + return ((uint)Value).ToString(); + } + + string ToStringFromEnd() => + '^' + Value.ToString(); +} +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/TcpClientExtensions.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/TcpClientExtensions.cs new file mode 100644 index 00000000..10b4a728 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/TcpClientExtensions.cs @@ -0,0 +1,14 @@ +#if NET461 + +namespace System.Net.Sockets; + +internal static class TcpClientExtensions +{ + public static async Task ConnectAsync(this TcpClient tcpClient, IPAddress address, int port, CancellationToken cancellationToken) + { + using var token = cancellationToken.Register(state => (state as TcpClient)!.Dispose(), tcpClient); + await tcpClient.ConnectAsync(address, port); + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj new file mode 100644 index 00000000..9566e560 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj @@ -0,0 +1,25 @@ + + + + net6.0;net461;net6.0-windows + enable + enable + latest + UiPath.Ipc.Extensions.BidirectionalHttp + UiPath.Ipc.Extensions.BidirectionalHttp + + + + + + + + + + + + + + + + diff --git a/src/UiPath.CoreIpc.Tests/ComputingTests.cs b/src/UiPath.CoreIpc.Tests/ComputingTests.cs index 6c562dd9..3fc1542a 100644 --- a/src/UiPath.CoreIpc.Tests/ComputingTests.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTests.cs @@ -1,129 +1,306 @@ -namespace UiPath.CoreIpc.Tests; +using Newtonsoft.Json; +using Nito.AsyncEx; +using Nito.Disposables; +using NSubstitute; +using System.Runtime.InteropServices; +using System.Text; +using UiPath.Ipc.Transport.NamedPipe; +using UiPath.Ipc.Transport.Tcp; +using UiPath.Ipc.Transport.WebSocket; +using Xunit.Abstractions; -public abstract class ComputingTests : TestBase where TBuilder : ServiceClientBuilder +namespace UiPath.Ipc.Tests; + +public abstract class ComputingTests : SpyTestBase { - protected readonly ServiceHost _computingHost; - protected readonly IComputingService _computingClient; - protected readonly ComputingService _computingService; - protected readonly ComputingCallback _computingCallback; - public ComputingTests() + #region " Setup " + protected readonly ComputingCallback _computingCallback = new(); + + private readonly Lazy _service; + private readonly Lazy _proxy; + + protected ComputingService Service => _service.Value; + protected IComputingService Proxy => _proxy.Value!; + + protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; + protected sealed override Type ContractType => typeof(IComputingService); + protected override ContractCollection? Callbacks => new() + { + { typeof(IComputingCallback), _computingCallback } + }; + + protected ComputingTests(ITestOutputHelper outputHelper) : base(outputHelper) { - _computingCallback = new ComputingCallback { Id = Guid.NewGuid().ToString() }; - _computingService = (ComputingService)_serviceProvider.GetService(); - _computingHost = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint() - .ValidateAndBuild(); - _computingHost.RunAsync(GuiScheduler); - _computingClient = ComputingClientBuilder(GuiScheduler).SerializeParametersAsObjects().ValidateAndBuild(); + ServiceProvider.InjectLazy(out _service); + CreateLazyProxy(out _proxy); } - protected abstract TBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null); - [Fact] - public async Task ReconnectWithEncrypt() + + protected override void ConfigureSpecificServices(IServiceCollection services) + => services + .AddSingleton() + .AddSingletonAlias() + .AddSingleton() + .AddSingletonAlias() + ; + + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; + #endregion + + [Theory, IpcAutoData] + public async Task Calls_ShouldWork(float x, float y) { - for (int i = 0; i < 50; i++) - { - await _computingClient.AddFloat(1, 2); - ((IpcProxy)_computingClient).CloseConnection(); - await _computingClient.AddFloat(1, 2); - } + await Proxy.AddFloats(x, y).ShouldBeAsync(x + y); } + [Theory, IpcAutoData] + public Task ConcurrentCalls_ShouldWork(float sameX, float sameY) => Task.WhenAll(Enumerable.Range(1, 50).Select(_ => Calls_ShouldWork(sameX, sameY))); + + [Theory, IpcAutoData] + public async Task CallsWithStructParamsAndReturns_ShouldWork(ComplexNumber a, ComplexNumber b) + => await Proxy.AddComplexNumbers(a, b).ShouldBeAsync(a + b); + [Fact] - public async Task AddFloat() + public async Task ClientCancellations_ShouldWork() { - var result = await _computingClient.AddFloat(1.23f, 4.56f); - result.ShouldBe(5.79f); + using var cts = new CancellationTokenSource(); + + var taskWaiting = Proxy.Wait(Timeout.InfiniteTimeSpan, cts.Token); + + await Task.Delay(Timeouts.Short); + + taskWaiting.IsCompleted.ShouldBeFalse(); + + cts.Cancel(); + + await taskWaiting.ShouldCompleteInAsync(Timeouts.Short).ShouldThrowAsync(); // in-process scheduling fast + + await Proxy.Wait(TimeSpan.Zero).ShouldCompleteInAsync(Timeouts.IpcRoundtrip).ShouldBeAsync(true); // connection still alive } + [Fact, OverrideConfig(typeof(ShortClientTimeout))] + public async Task ClientTimeouts_ShouldWork() + { + await Proxy.Wait(Timeout.InfiniteTimeSpan).ShouldThrowAsync(); + + await Proxy.GetCallbackThreadName( + waitOnServer: TimeSpan.Zero, + message: new() + { + RequestTimeout = Timeouts.DefaultRequest + }) + .ShouldBeAsync(Names.GuiThreadName) + .ShouldNotThrowAsync(); + } + + private sealed class ShortClientTimeout : OverrideConfig + { + public override IpcClient? Override(Func client) => client().WithRequestTimeout(TimeSpan.FromMilliseconds(10)); + } + + [Theory, IpcAutoData] + public async Task CallsWithArraysOfStructsAsParams_ShouldWork(ComplexNumber a, ComplexNumber b, ComplexNumber c) + => await Proxy.AddComplexNumberList([a, b, c]).ShouldBeAsync(a + b + c); + + [Fact] + public async Task Callbacks_ShouldWork() + => await Proxy.GetCallbackThreadName(waitOnServer: TimeSpan.Zero).ShouldBeAsync(Names.GuiThreadName); + [Fact] - public Task AddFloatConcurrently() => Task.WhenAll(Enumerable.Range(1, 100).Select(_ => AddFloat())); + public async Task CallbacksWithParams_ShouldWork() + => await Proxy.MultiplyInts(7, 1).ShouldBeAsync(7); [Fact] - public async Task AddComplexNumber() + public async Task ConcurrentCallbacksWithParams_ShouldWork() + => await Task.WhenAll( + Enumerable.Range(1, 50).Select(_ => CallbacksWithParams_ShouldWork())); + + [Fact] + public async Task BeforeCall_ShouldApplyToCallsButNotToToCallbacks() { - var result = await _computingClient.AddComplexNumber(new ComplexNumber(1f, 3f), new ComplexNumber(2f, 5f)); - result.ShouldBe(new ComplexNumber(3f, 8f)); + await Proxy.GetCallbackThreadName(TimeSpan.Zero).ShouldBeAsync(Names.GuiThreadName); + + _clientBeforeCalls.ShouldContain(x => x.Method.Name == nameof(IComputingService.GetCallbackThreadName)); + _clientBeforeCalls.ShouldNotContain(x => x.Method.Name == nameof(IComputingCallback.GetThreadName)); + + _serverBeforeCalls.ShouldContain(x => x.Method.Name == nameof(IComputingService.GetCallbackThreadName)); + _serverBeforeCalls.ShouldNotContain(x => x.Method.Name == nameof(IComputingCallback.GetThreadName)); } [Fact] - public async Task ClientCancellation() + public async Task ServerBeforeCall_WhenSync_ShouldShareAsyncLocalContextWithTheTargetMethodCall() { - using (var cancellationSource = new CancellationTokenSource(10)) + await Proxy.GetCallContext().ShouldBeAsync(null); + + var id = $"{Guid.NewGuid():N}"; + var expectedCallContext = $"{nameof(IComputingService.GetCallContext)}-{id}"; + + _tailBeforeCall = (callInfo, _) => { - _computingClient.Infinite(cancellationSource.Token).ShouldThrow(); - } - await AddFloat(); + ComputingService.Context = $"{callInfo.Method.Name}-{id}"; + return Task.CompletedTask; + }; + + await Proxy.GetCallContext().ShouldBeAsync(expectedCallContext); } [Fact] - public async Task ClientTimeout() + [OverrideConfig(typeof(SetBeforeConnect))] + public async Task BeforeConnect_ShouldWork() { - var proxy = ComputingClientBuilder().SerializeParametersAsObjects().RequestTimeout(TimeSpan.FromMilliseconds(10)).ValidateAndBuild(); - proxy.Infinite().ShouldThrow().Message.ShouldBe($"{nameof(_computingClient.Infinite)} timed out."); - await proxy.GetCallbackThreadName(new Message { RequestTimeout = RequestTimeout }); - ((IDisposable)proxy).Dispose(); - ((IpcProxy)proxy).CloseConnection(); + int callCount = 0; + SetBeforeConnect.Set(async _ => callCount++); + + await Proxy.AddFloats(1, 2).ShouldBeAsync(3); + callCount.ShouldBe(1); + + await Proxy.AddFloats(1, 2).ShouldBeAsync(3); + callCount.ShouldBe(1); + + await (IpcProxy?.CloseConnection() ?? default); + await Proxy.AddFloats(1, 2).ShouldBeAsync(3); + callCount.ShouldBe(2); } - [Fact] - public async Task TimeoutPerRequest() + private sealed class SetBeforeConnect : OverrideConfig + { + private static readonly AsyncLocal ValueStorage = new(); + public static void Set(BeforeConnectHandler value) => ValueStorage.Value = value; + + public override IpcClient? Override(Func client) + => client().WithBeforeConnect(ct => ValueStorage.Value.ShouldNotBeNull().Invoke(ct)); + } + +#if !NET461 && !CI + [SkippableFact] +#endif + [OverrideConfig(typeof(DisableInProcClientServer))] + public async Task BeforeConnect_ShouldStartExternalServerJIT() { - for (int i = 0; i < 20; i++) + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test works only on Windows."); + + using var whereDotNet = new Process { - var request = new SystemMessage { RequestTimeout = TimeSpan.FromTicks(10), Delay = 100 }; - Exception exception = null; - try + StartInfo = { - await _computingClient.SendMessage(request); + FileName = "where.exe", + Arguments = "dotnet.exe", } - catch (TimeoutException ex) + }; + var pathDotNet = await whereDotNet.RunReturnStdOut(); + + var externalServerParams = RandomServerParams(); + var arg = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(externalServerParams))); + + var pipeName = $"{Guid.NewGuid():N}"; + + using var serverProcess = new Process + { + StartInfo = + { + FileName = pathDotNet, + Arguments = $"\"{Assembly.GetExecutingAssembly().Location}\" {arg}", + UseShellExecute = false, + }, + }; + using var killProcess = new Disposable(() => + { + try { - exception = ex; + serverProcess.Kill(); } - catch (RemoteException ex) + catch { - exception = ex; - ex.Is().ShouldBeTrue(); } - exception.Message.ShouldBe($"{nameof(_computingClient.SendMessage)} timed out."); - await AddFloat(); - } - } + _outputHelper.WriteLine("Killed server process"); + }); + var proxy = new IpcClient + { + Scheduler = GuiScheduler, + BeforeConnect = async (_) => + { + serverProcess.Start(); + var time = TimeSpan.FromSeconds(1); + _outputHelper.WriteLine($"Server started. Waiting {time}. PID={serverProcess.Id}"); + await Task.Delay(time); + }, + Transport = externalServerParams.CreateClientTransport() + }.GetProxy(); - [Fact] - public Task InfiniteVoid() => _computingClient.InfiniteVoid(); + await proxy.AddFloats(1, 2).ShouldBeAsync(3); + } - [Fact] - public async Task AddComplexNumbers() + [SkippableFact] + public async Task ManyConnections_ShouldWork() { - var result = await _computingClient.AddComplexNumbers(new[] - { - new ComplexNumber(0.5f, 0.4f), - new ComplexNumber(0.2f, 0.1f), - new ComplexNumber(0.3f, 0.5f), - }); - result.ShouldBe(new ComplexNumber(1f, 1f)); - } + const int CParallelism = 10; + const int CTimesEach = 100; - [Fact] - public async Task GetCallbackThreadName() => (await _computingClient.GetCallbackThreadName()).ShouldBe("GuiThread"); + await Enumerable.Range(1, CParallelism) + .Select(async index => + { + var mockCallback = Substitute.For(); + mockCallback.AddInts(0, 1).Returns(1); - [Fact] - public Task CallbackConcurrently() => Task.WhenAll(Enumerable.Range(1, 50).Select(_ => Callback())); + var proxy = CreateIpcClient(callbacks: new() + { + { typeof(IComputingCallback), mockCallback } + })!.GetProxy(); - [Fact] - public async Task Callback() + foreach (var time in Enumerable.Range(1, CTimesEach)) + { + await (proxy as IpcProxy)!.CloseConnection(); + + mockCallback.ClearReceivedCalls(); + await proxy.MultiplyInts(1, 1).ShouldBeAsync(1); + await mockCallback.Received().AddInts(0, 1); + } + }) + .WhenAll(); + } + + public abstract IAsyncDisposable? RandomTransportPair(out ServerTransport listener, out ClientTransport transport); + + public abstract ExternalServerParams RandomServerParams(); + public readonly record struct ExternalServerParams(ServerKind Kind, string? PipeName = null, int Port = 0) { - var message = new SystemMessage { Text = Guid.NewGuid().ToString() }; - var returnValue = await _computingClient.SendMessage(message); - returnValue.ShouldBe($"{Environment.UserName}_{_computingCallback.Id}_{message.Text}"); + public IAsyncDisposable? CreateListenerConfig(out ServerTransport listenerConfig) + { + switch (Kind) + { + case ServerKind.NamedPipes: + { + listenerConfig = new NamedPipeServerTransport() { PipeName = PipeName! }; + return null; + } + case ServerKind.Tcp: + { + listenerConfig = new TcpServerTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }; + return null; + } + case ServerKind.WebSockets: + { + var context = new WebSocketContext(Port); + listenerConfig = new WebSocketServerTransport { Accept = context.Accept }; + return context; + } + default: + throw new NotSupportedException($"Kind not supported. Kind was {Kind}"); + } + } + + public ClientTransport CreateClientTransport() => Kind switch + { + ServerKind.NamedPipes => new NamedPipeClientTransport() { PipeName = PipeName! }, + ServerKind.Tcp => new TcpClientTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }, + ServerKind.WebSockets => new WebSocketClientTransport() { Uri = new($"ws://localhost:{Port}") }, + _ => throw new NotSupportedException($"Kind not supported. Kind was {Kind}") + }; } + public enum ServerKind { NamedPipes, Tcp, WebSockets } - public override void Dispose() + private sealed class DisableInProcClientServer : OverrideConfig { - ((IDisposable)_computingClient).Dispose(); - ((IpcProxy)_computingClient).CloseConnection(); - _computingHost.Dispose(); - base.Dispose(); + public override async Task Override(Func> ipcServerFactory) => null; + public override IpcClient? Override(Func client) => null; } -} \ No newline at end of file +} diff --git a/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs new file mode 100644 index 00000000..88ef854f --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs @@ -0,0 +1,32 @@ +using UiPath.Ipc.Transport.NamedPipe; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public sealed class ComputingTestsOverNamedPipes : ComputingTests +{ + private string PipeName => Names.GetPipeName(role: "computing", TestRunId); + + public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected override async Task CreateServerTransport() => new NamedPipeServerTransport + { + PipeName = PipeName + }; + protected override ClientTransport CreateClientTransport() => new NamedPipeClientTransport + { + PipeName = PipeName, + AllowImpersonation = true, + }; + + public override IAsyncDisposable? RandomTransportPair(out ServerTransport listener, out ClientTransport transport) + { + var pipeName = $"{Guid.NewGuid():N}"; + listener = new NamedPipeServerTransport { PipeName = pipeName }; + transport = new NamedPipeClientTransport { PipeName = pipeName }; + return null; + } + + public override ExternalServerParams RandomServerParams() + => new(ServerKind.NamedPipes, PipeName: $"{Guid.NewGuid():N}"); +} diff --git a/src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs new file mode 100644 index 00000000..96233fda --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs @@ -0,0 +1,35 @@ +using System.Net; +using UiPath.Ipc.Transport.Tcp; +using Xunit.Abstractions; + +namespace UiPath.CoreIpc.Tests; + +public sealed class ComputingTestsOverTcp : ComputingTests +{ + private readonly IPEndPoint _endPoint = NetworkHelper.FindFreeLocalPort(); + + public ComputingTestsOverTcp(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected override async Task CreateListener() + => new TcpListener + { + EndPoint = _endPoint, + }; + + protected override ClientTransport CreateClientTransport() + => new TcpTransport() { EndPoint = _endPoint }; + + public override IAsyncDisposable? RandomTransportPair(out ListenerConfig listener, out ClientTransport transport) + { + var endPoint = NetworkHelper.FindFreeLocalPort(); + listener = new TcpListener() { EndPoint = endPoint }; + transport = new TcpTransport() { EndPoint = endPoint }; + return null; + } + + public override ExternalServerParams RandomServerParams() + { + var endPoint = NetworkHelper.FindFreeLocalPort(); + return new(ServerKind.Tcp, Port: endPoint.Port); + } +} diff --git a/src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs new file mode 100644 index 00000000..4df0126d --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs @@ -0,0 +1,44 @@ +using UiPath.Ipc.Transport.WebSocket; +using Xunit.Abstractions; + +namespace UiPath.CoreIpc.Tests; + +public sealed class ComputingTestsOverWebSockets : ComputingTests +{ + private readonly WebSocketContext _webSocketContext = new(); + + public ComputingTestsOverWebSockets(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected override async Task DisposeAsync() + { + await _webSocketContext.DisposeAsync(); + await base.DisposeAsync(); + } + + protected override async Task CreateListener() + { + var listener = new WebSocketListener + { + Accept = _webSocketContext.Accept, + }; + await Task.Delay(200); // Wait for the listener to start. + return listener; + } + + protected override ClientTransport CreateClientTransport() + => new WebSocketTransport() { Uri = _webSocketContext.ClientUri }; + + public override IAsyncDisposable? RandomTransportPair(out ListenerConfig listener, out ClientTransport transport) + { + var context = new WebSocketContext(); + listener = new WebSocketListener() { Accept = context.Accept }; + transport = new WebSocketTransport() { Uri = context.ClientUri }; + return context; + } + + public override ExternalServerParams RandomServerParams() + { + var endPoint = NetworkHelper.FindFreeLocalPort(); + return new(ServerKind.WebSockets, Port: endPoint.Port); + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs new file mode 100644 index 00000000..cecdedde --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs @@ -0,0 +1,39 @@ +namespace UiPath.Ipc.Tests; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +internal sealed class OverrideConfigAttribute : Attribute +{ + public Type OverrideConfigType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A typeof expression that indicates a concrete subclass of , with a public, parameterless constructor. + public OverrideConfigAttribute(Type overrideConfigType) + { + if (overrideConfigType is null) + { + throw new ArgumentNullException(nameof(overrideConfigType)); + } + if (overrideConfigType.IsAbstract) + { + throw new ArgumentException($"The type {overrideConfigType} is abstract.", nameof(overrideConfigType)); + } + if (!typeof(OverrideConfig).IsAssignableFrom(overrideConfigType)) + { + throw new ArgumentException($"The type {overrideConfigType} does not inherit from {typeof(OverrideConfig)}.", nameof(overrideConfigType)); + } + if (overrideConfigType.GetConstructor(BindingFlags.Public | BindingFlags.Instance, Type.DefaultBinder, Type.EmptyTypes, modifiers: null) is null) + { + throw new ArgumentException($"The type {overrideConfigType} does not have a public, parameterless constructor.", nameof(overrideConfigType)); + } + + OverrideConfigType = overrideConfigType; + } +} + +public abstract class OverrideConfig +{ + public virtual Task Override(Func> ipcServer) => ipcServer()!; + public virtual IpcClient? Override(Func ipcClientFactory) => ipcClientFactory(); +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/EndpointTests.cs b/src/UiPath.CoreIpc.Tests/EndpointTests.cs deleted file mode 100644 index e62db908..00000000 --- a/src/UiPath.CoreIpc.Tests/EndpointTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -namespace UiPath.CoreIpc.Tests; - -public class EndpointTests : IDisposable -{ - private static TimeSpan RequestTimeout => TestBase.RequestTimeout; - private readonly ServiceHost _host; - private readonly IComputingService _computingClient; - private readonly ISystemService _systemClient; - private readonly ComputingService _computingService; - private readonly SystemService _systemService; - private readonly ComputingCallback _computingCallback; - private readonly SystemCallback _systemCallback; - private readonly IServiceProvider _serviceProvider; - public EndpointTests() - { - _computingCallback = new ComputingCallback { Id = Guid.NewGuid().ToString() }; - _systemCallback = new SystemCallback { Id = Guid.NewGuid().ToString() }; - _serviceProvider = IpcHelpers.ConfigureServices(); - _computingService = (ComputingService)_serviceProvider.GetService(); - _systemService = (SystemService)_serviceProvider.GetService(); - _host = new ServiceHostBuilder(_serviceProvider) - .UseNamedPipes(new NamedPipeSettings(PipeName) { RequestTimeout = RequestTimeout }) - .AddEndpoint() - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - _host.RunAsync(); - _computingClient = ComputingClientBuilder().ValidateAndBuild(); - _systemClient = CreateSystemService(); - } - public string PipeName => nameof(EndpointTests)+GetHashCode(); - private NamedPipeClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => - new NamedPipeClientBuilder(PipeName, _serviceProvider) - .AllowImpersonation() - .RequestTimeout(RequestTimeout) - .CallbackInstance(_computingCallback) - .SerializeParametersAsObjects() - .TaskScheduler(taskScheduler); - private ISystemService CreateSystemService() => SystemClientBuilder().ValidateAndBuild(); - private NamedPipeClientBuilder SystemClientBuilder() => - new NamedPipeClientBuilder(PipeName, _serviceProvider) - .CallbackInstance(_systemCallback) - .SerializeParametersAsObjects() - .RequestTimeout(RequestTimeout) - .AllowImpersonation(); - public void Dispose() - { - ((IDisposable)_computingClient).Dispose(); - ((IDisposable)_systemClient).Dispose(); - ((IpcProxy)_computingClient).CloseConnection(); - ((IpcProxy)_systemClient).CloseConnection(); - _host.Dispose(); - } - [Fact] - public Task CallbackConcurrently() => Task.WhenAll(Enumerable.Range(1, 50).Select(_ => CallbackCore())); - [Fact] - public async Task Callback() - { - for (int index = 0; index < 50; index++) - { - await CallbackCore(); - ((IpcProxy)_computingClient).CloseConnection(); - } - } - - private async Task CallbackCore() - { - var proxy = new NamedPipeClientBuilder(PipeName) - .SerializeParametersAsObjects().RequestTimeout(RequestTimeout).AllowImpersonation().ValidateAndBuild(); - var message = new SystemMessage { Text = Guid.NewGuid().ToString() }; - var computingTask = _computingClient.SendMessage(message); - var systemTask = _systemClient.SendMessage(message); - var computingBaseTask = proxy.AddFloat(1, 2); - await Task.WhenAll(computingTask, systemTask, computingBaseTask); - systemTask.Result.ShouldBe($"{Environment.UserName}_{_systemCallback.Id}_{message.Text}"); - computingTask.Result.ShouldBe($"{Environment.UserName}_{_computingCallback.Id}_{message.Text}"); - computingBaseTask.Result.ShouldBe(3); - } - - [Fact] - public async Task MissingCallback() - { - RemoteException exception = null; - try - { - await _systemClient.MissingCallback(new SystemMessage()); - } - catch (RemoteException ex) - { - exception = ex; - } - exception.Message.ShouldBe("Callback contract mismatch. Requested System.IDisposable, but it's UiPath.CoreIpc.Tests.ISystemCallback."); - exception.Is().ShouldBeTrue(); - } - [Fact] - public Task CancelServerCall() => CancelServerCallCore(10); - - async Task CancelServerCallCore(int counter) - { - for (int i = 0; i < counter; i++) - { - var request = new SystemMessage { RequestTimeout = Timeout.InfiniteTimeSpan, Delay = Timeout.Infinite }; - Task sendMessageResult; - using (var cancellationSource = new CancellationTokenSource()) - { - sendMessageResult = _systemClient.MissingCallback(request, cancellationSource.Token); - var newGuid = Guid.NewGuid(); - (await _systemClient.GetGuid(newGuid)).ShouldBe(newGuid); - await Task.Delay(1); - cancellationSource.Cancel(); - sendMessageResult.ShouldThrow(); - newGuid = Guid.NewGuid(); - (await _systemClient.GetGuid(newGuid)).ShouldBe(newGuid); - } - ((IDisposable)_systemClient).Dispose(); - } - } - - [Fact] - public async Task DuplicateCallbackProxies() - { - await _systemClient.GetThreadName(); - var proxy = CreateSystemService(); - var message = proxy.GetThreadName().ShouldThrow().Message; - message.ShouldStartWith("Duplicate callback proxy instance EndpointTests"); - message.ShouldEndWith(". Consider using a singleton callback proxy."); - } -} -public interface ISystemCallback -{ - Task GetId(Message message = null); -} -public class SystemCallback : ISystemCallback -{ - public string Id { get; set; } - public async Task GetId(Message message) - { - message.Client.ShouldBeNull(); - return Id; - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/GlobalUsings.cs b/src/UiPath.CoreIpc.Tests/GlobalUsings.cs new file mode 100644 index 00000000..74b2b413 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using UiPath.Ipc; +global using Accept = System.Func>; +global using BeforeConnectHandler = System.Func; +global using BeforeCallHandler = System.Func; diff --git a/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs new file mode 100644 index 00000000..fd750042 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Hosting; + +namespace UiPath.Ipc.Tests; + +public static class DiExtensions +{ + public static void InjectLazy(this IServiceProvider serviceProvider, out Lazy lazy) + where T : class + => lazy = new(serviceProvider.GetRequiredService); + + public static IServiceCollection AddHostedSingleton(this IServiceCollection services) + where TService : class + where THostedImpl : class, TService, IHostedService + => services + .AddSingleton() + .AddHostedService(sp => (THostedImpl)sp.GetRequiredService()); +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs new file mode 100644 index 00000000..257e9a8b --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs @@ -0,0 +1,72 @@ +using System.Net.WebSockets; +using System.Net; +using System.Threading.Channels; + +namespace UiPath.Ipc.Tests; + +public class HttpSysWebSocketsListener : IAsyncDisposable +{ + private readonly CancellationTokenSource _cts = new(); + private readonly HttpListener _httpListener = new(); + private readonly Channel _channel = Channel.CreateBounded(capacity: 5); + private readonly Task _processingContexts; + + public HttpSysWebSocketsListener(string uriPrefix) + { + _httpListener.Prefixes.Add(uriPrefix); + _httpListener.Start(); + + _processingContexts = ProcessContexts(_cts.Token); + } + + private async Task ProcessContexts(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + var context = await _httpListener.GetContextAsync(); + await _channel.Writer.WriteAsync(context, ct); + } + _channel.Writer.Complete(); + } + catch (Exception ex) + { + _channel.Writer.Complete(ex); + } + } + + public async Task Accept(CancellationToken ct) + { + while (true) + { + var listenerContext = await _channel.Reader.ReadAsync(ct); + if (listenerContext.Request.IsWebSocketRequest) + { + var webSocketContext = await listenerContext.AcceptWebSocketAsync(subProtocol: null); + return webSocketContext.WebSocket; + } + listenerContext.Response.StatusCode = 400; + listenerContext.Response.Close(); + } + } + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + _httpListener.Stop(); + + try + { + await _processingContexts; + } + catch (ObjectDisposedException) + { + // ignore + } + catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token) + { + // ignore + } + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs new file mode 100644 index 00000000..2ce875a1 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs @@ -0,0 +1,19 @@ +using AutoFixture; +using AutoFixture.Xunit2; + +namespace UiPath.Ipc.Tests; + +internal class IpcAutoDataAttribute : AutoDataAttribute +{ + public IpcAutoDataAttribute() : base(CreateFixture) + { + } + + public static Fixture CreateFixture() + { + var fixture = new Fixture(); + new SupportMutableValueTypesCustomization().Customize(fixture); + return fixture; + } +} + diff --git a/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs new file mode 100644 index 00000000..1f0c1217 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +using SP = ServiceProviderServiceExtensions; + +internal static class IpcHelpers +{ + public static ServiceProvider ConfigureServices(ITestOutputHelper outputHelper, Action? configureSpecificServices = null) + { + var services = new ServiceCollection() + .AddLogging(builder => builder + .AddTraceSource(new SourceSwitch("", "All")) + .AddXUnit(outputHelper)); + + configureSpecificServices?.Invoke(services); + + return services + .BuildServiceProvider(); + } + + public static IServiceCollection AddSingletonAlias(this IServiceCollection services) + where TNew : class + where TExisting : class, TNew + => services.AddSingleton(SP.GetRequiredService); + + public static IServiceProvider GetRequired(this IServiceProvider serviceProvider, out T service) where T : class + { + service = serviceProvider.GetRequiredService(); + return serviceProvider; + } +} + +internal static class IpcClientExtensions +{ + public static IpcClient WithRequestTimeout(this IpcClient ipcClient, TimeSpan requestTimeout) + { + ipcClient.RequestTimeout = requestTimeout; + return ipcClient; + } + public static IpcServer WithRequestTimeout(this IpcServer ipcServer, TimeSpan requestTimeout) + { + ipcServer.RequestTimeout = requestTimeout; + return ipcServer; + } + public static async Task WithRequestTimeout(this Task ipcServerTask, TimeSpan requestTimeout) + => (await ipcServerTask).WithRequestTimeout(requestTimeout); + + public static IpcClient WithCallbacks(this IpcClient ipcClient, ContractCollection callbacks) + { + ipcClient.Callbacks = callbacks; + return ipcClient; + } + + public static IpcClient WithBeforeConnect(this IpcClient ipcClient, BeforeConnectHandler beforeConnect) + { + ipcClient.BeforeConnect = beforeConnect; + return ipcClient; + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Helpers/Names.cs b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs new file mode 100644 index 00000000..2668055b --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs @@ -0,0 +1,8 @@ +namespace UiPath.Ipc.Tests; + +internal static class Names +{ + public const string GuiThreadName = "GuiThread"; + + public static string GetPipeName(string role, TestRunId testRunId) => $"{role}_{testRunId.Value:N}"; +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs new file mode 100644 index 00000000..327ffbd1 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Net.Sockets; + +namespace UiPath.Ipc.Tests; + +public static class NetworkHelper +{ + public static IPEndPoint FindFreeLocalPort() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return (IPEndPoint)socket.LocalEndPoint!; + } +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs new file mode 100644 index 00000000..0895c94d --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs @@ -0,0 +1,117 @@ +using System.Runtime.CompilerServices; + +namespace UiPath.Ipc.Tests; + +[ShouldlyMethods] +internal static class ShouldlyHelpers +{ + public static async Task ShouldBeAsync(this Task task, T expected, [CallerArgumentExpression(nameof(task))] string? taskExpression = null, bool launchDebugger = false) + { + var actual = await task; + try + { + actual.ShouldBe(expected); + } + catch (Exception ex) + { + if (launchDebugger) + { + Debugger.Launch(); + } + throw new ShouldAssertException($"Awaiting the expression `{taskExpression}`\r\n\tshould yield\r\n{expected}\r\n\tbut actually yielded\r\n{actual}", ex); + } + } + + public static async Task ShouldNotBeNullAsync(this Task task, [CallerArgumentExpression(nameof(task))] string? taskExpression = null) + where T : class + { + var actual = await task; + try + { + return actual.ShouldNotBeNull(); + } + catch + { + throw new ShouldAssertException($"The provided expression `{taskExpression}`\r\n\tshouldn't have yielded null but did."); + } + } + + + public static async Task ShouldNotThrowAsyncAnd(this Task task, [CallerArgumentExpression(nameof(task))] string? taskExpression = null) + { + try + { + return await task; + } + catch (Exception ex) + { + throw new ShouldAssertException($"The provided expression `{taskExpression}`\r\n\tshould not throw but threw\r\n{ex.GetType().FullName}\r\n\twith message\r\n\"{ex.Message}\"", ex); + } + } + + + public static Task ShouldCompleteInAsync(this Task task, TimeSpan lease, [CallerArgumentExpression(nameof(task))] string? taskExpression = null) + => task.Return(0).ShouldCompleteInAsync(lease, taskExpression); + + public static async Task ShouldCompleteInAsync(this Task task, TimeSpan lease, [CallerArgumentExpression(nameof(task))] string? taskExpression = null) + { + using var cts = new CancellationTokenSource(lease); + try + { + return await Nito.AsyncEx.TaskExtensions.WaitAsync(task, cts.Token); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) + { + throw new ShouldCompleteInException($"The task {taskExpression} should complete in {lease} but did not.", inner: null); + } + } + + public static async Task ShouldStallForAtLeastAsync(this Task task, TimeSpan lease, [CallerArgumentExpression(nameof(task))] string? taskExpression = null) + { + using var cts = new CancellationTokenSource(lease); + try + { + _ = await Nito.AsyncEx.TaskExtensions.WaitAsync(task, cts.Token); + throw new ShouldAssertException($"The task {taskExpression} should stall for at least {lease} but it completed faster."); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) + { + } + } + + + public static async Task ShouldSatisfyAllConditionsAsync(this Task task, Action[] assertions, [CallerArgumentExpression(nameof(task))] string? taskExpression = null, [CallerArgumentExpression(nameof(assertions))] string? assertionsExpression = null) + { + var actual = await task; + var exceptions = new List(); + foreach (var assertion in assertions) + { + try + { + assertion(actual); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + + var innerException = exceptions switch + { + [] => null, + [var singleException] => singleException, + _ => new AggregateException(exceptions) + }; + + if (innerException is not null) + { + throw new ShouldAssertException($"Awaiting the expression `{taskExpression}`\r\n\tshould yield a value that satisfies these assertions\r\n{assertionsExpression}\r\n\tbut at least one assertion was not satisfied.", innerException); + } + } + private static async Task Return(this Task task, T value = default!) + { + await task; + return value; + } +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs new file mode 100644 index 00000000..cbf2c599 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs @@ -0,0 +1,15 @@ +namespace UiPath.Ipc.Tests; + +internal abstract class StreamBase : Stream + { + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + } \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs new file mode 100644 index 00000000..82174e14 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs @@ -0,0 +1,27 @@ +namespace UiPath.Ipc.Tests; + +public static class StreamExtensions +{ + public static async Task ReadExactlyAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken ct = default) + { + while (length > 0) + { + var cbRead = await stream.ReadAsync(buffer, offset, length, ct); + if (cbRead == 0) + { + throw new EndOfStreamException(); + } + + offset += cbRead; + length -= cbRead; + } + } + + public static async Task ReadToEndAsync(this Stream stream, CancellationToken ct = default) + { + using var memory = new MemoryStream(); + await stream.CopyToAsync(memory, bufferSize: 8192, ct); + return memory.ToArray(); + } + +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs new file mode 100644 index 00000000..f76d43c8 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs @@ -0,0 +1,32 @@ +namespace UiPath.Ipc.Tests; + +public readonly record struct TestRunId(Guid Value) +{ + public static TestRunId New() => new(Guid.NewGuid()); +} + +public static class ProcessHelper +{ + public static async Task RunReturnStdOut(this Process process) + { + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + + TaskCompletionSource tcsProcessExited = new(); + process.EnableRaisingEvents = true; + process.Exited += (_, _) => tcsProcessExited.SetResult(null); + + _ = process.Start(); + await tcsProcessExited.Task; + + var stdOut = await process.StandardOutput.ReadToEndAsync(); + if (process.ExitCode is not 0) + { + var stdErr = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"The process exited with a non zero code. ExitCode={process.ExitCode}\r\nStdOut:\r\n{stdOut}\r\n\r\nStdErr:\r\n{stdErr}"); + } + + return stdOut; + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs new file mode 100644 index 00000000..d09e1e2e --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs @@ -0,0 +1,12 @@ +namespace UiPath.Ipc.Tests; + +internal static class Timeouts +{ + public static readonly TimeSpan LocalProxyToThrowOCE = TimeSpan.FromMilliseconds(200); + + public static readonly TimeSpan IpcRoundtrip = TimeSpan.FromMilliseconds(800); + + public static readonly TimeSpan Short = TimeSpan.FromMilliseconds(300); + + public static readonly TimeSpan DefaultRequest = Debugger.IsAttached ? TimeSpan.FromDays(1) : TimeSpan.FromMinutes(1); +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs new file mode 100644 index 00000000..e9648056 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs @@ -0,0 +1,16 @@ +namespace UiPath.Ipc.Tests; + +internal sealed class TracedStream(Stream target) : StreamBase +{ + private readonly MemoryStream _bytes = new(); + + public byte[] GetTrace() => _bytes.ToArray(); + + public override long Length => target.Length; + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var cbRead = await target.ReadAsync(buffer, offset, count, cancellationToken); + _bytes.Write(buffer, offset, cbRead); + return cbRead; + } +} diff --git a/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs new file mode 100644 index 00000000..5fb55217 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs @@ -0,0 +1,30 @@ +namespace UiPath.Ipc.Tests; + +internal sealed class WebSocketContext : IAsyncDisposable +{ + private readonly HttpSysWebSocketsListener _httpListener; + + public Accept Accept => _httpListener.Accept; + public Uri ClientUri { get; } + + public WebSocketContext(int? port = null) + { + var actualPort = port ?? NetworkHelper.FindFreeLocalPort().Port; + ClientUri = Uri("ws"); + _httpListener = new(uriPrefix: Uri("http").ToString()); + + Uri Uri(string scheme) => new UriBuilder(scheme, "localhost", actualPort).Uri; + } + + public async ValueTask DisposeAsync() + { + try + { + await _httpListener.DisposeAsync(); + } + catch (Exception ex) + { + Trace.TraceError($"Disposing the http listener threw: {ex}"); + } + } +} diff --git a/src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs b/src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs deleted file mode 100644 index 7938f812..00000000 --- a/src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace UiPath.CoreIpc.Tests; - -public interface IComputingCallback -{ - Task GetId(Message message); - Task GetThreadName(); -} -public class ComputingCallback : IComputingCallback -{ - public string Id { get; set; } - public async Task GetId(Message message) - { - message.Client.ShouldBeNull(); - return Id; - } - - public async Task GetThreadName() => Thread.CurrentThread.Name; -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs b/src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs deleted file mode 100644 index 908ee1f0..00000000 --- a/src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace UiPath.CoreIpc.Tests; - -public interface IInvalid : IDisposable -{ -} - -public interface IDuplicateMessage -{ - Task Test(Message message1, Message message2); -} - -public interface IUploadNotification -{ - Task Upload(Stream stream); -} - -public interface IDerivedStreamDownload -{ - Task Download(); -} - -public interface IDuplicateStreams -{ - Task Upload(Stream stream, Stream stream2); -} - -public interface IDerivedStreamUpload -{ - Task Upload(MemoryStream stream); -} - -public interface IMessageFirst -{ - Task Test(Message message1, int x); -} - -public interface IInvalidCancellationToken -{ - Task Test(CancellationToken token, int x); -} - -public interface IComputingServiceBase -{ - Task AddFloat(float x, float y, CancellationToken cancellationToken = default); -} -public interface IComputingService : IComputingServiceBase -{ - Task AddComplexNumber(ComplexNumber x, ComplexNumber y, CancellationToken cancellationToken = default); - Task AddComplexNumbers(IEnumerable numbers, CancellationToken cancellationToken = default); - Task SendMessage(SystemMessage message, CancellationToken cancellationToken = default); - Task Infinite(CancellationToken cancellationToken = default); - Task InfiniteVoid(CancellationToken cancellationToken = default); - Task GetCallbackThreadName(Message message = null, CancellationToken cancellationToken = default); -} - -public struct ComplexNumber -{ - public float A { get; set; } - public float B { get; set; } - - public ComplexNumber(float a, float b) - { - A = a; - B = b; - } -} - -public enum TextStyle -{ - TitleCase, - Upper -} - -public class ConvertTextArgs -{ - public TextStyle TextStyle { get; set; } = TextStyle.Upper; - - public string Text { get; set; } = string.Empty; -} - -public class ComputingService : IComputingService -{ - private readonly ILogger _logger; - - public ComputingService(ILogger logger) // inject dependencies in constructor - { - _logger = logger; - } - - public async Task AddComplexNumber(ComplexNumber x, ComplexNumber y, CancellationToken cancellationToken = default) - { - _logger.LogInformation($"{nameof(AddComplexNumber)} called."); - return new ComplexNumber(x.A + y.A, x.B + y.B); - } - - public async Task AddComplexNumbers(IEnumerable numbers, CancellationToken cancellationToken = default) - { - _logger.LogInformation($"{nameof(AddComplexNumbers)} called."); - var result = new ComplexNumber(0, 0); - foreach (ComplexNumber number in numbers) - { - result = new ComplexNumber(result.A + number.A, result.B + number.B); - } - return result; - } - - public async Task AddFloat(float x, float y, CancellationToken cancellationToken = default) - { - //Trace.WriteLine($"{nameof(AddFloat)} called."); - _logger.LogInformation($"{nameof(AddFloat)} called."); - return x + y; - } - - public async Task Infinite(CancellationToken cancellationToken = default) - { - await Task.Delay(Timeout.Infinite, cancellationToken); - return true; - } - - public Task InfiniteVoid(CancellationToken cancellationToken = default) =>Task.Delay(Timeout.Infinite, cancellationToken); - - public async Task SendMessage(SystemMessage message, CancellationToken cancellationToken = default) - { - await Task.Delay(message.Delay, cancellationToken); - var client = message.Client; - var callback = message.GetCallback(); - var clientId = await callback.GetId(message); - string returnValue = ""; - client.Impersonate(() => returnValue = client.GetUserName() + "_" + clientId + "_" + message.Text); - return returnValue; - } - - public async Task GetCallbackThreadName(Message message, CancellationToken cancellationToken = default) => await message.GetCallback().GetThreadName(); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs b/src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs deleted file mode 100644 index 41a9292f..00000000 --- a/src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Net; -using System.Net.WebSockets; -using UiPath.CoreIpc.Tests; - -namespace UiPath.CoreIpc; - -public static class IpcHelpers -{ - public static TInterface ValidateAndBuild(this ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder - { -#if DEBUG - Validator.Validate(builder); -#endif - return builder.Build(); - } - public static ServiceHost ValidateAndBuild(this ServiceHostBuilder serviceHostBuilder) - { -#if DEBUG - Validator.Validate(serviceHostBuilder); -#endif - return serviceHostBuilder.Build(); - } - public static IServiceProvider ConfigureServices() => - new ServiceCollection() - .AddLogging(b => b.AddTraceSource(new SourceSwitch("", "All"))) - .AddIpc() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); - public static string GetUserName(this IClient client) - { - string userName = null; - client.Impersonate(() => userName = Environment.UserName); - return userName; - } - public static IServiceCollection AddIpcWithLogging(this IServiceCollection services, bool logToConsole = false) - { - services.AddLogging(builder => - { - //if (logToConsole) - //{ - // builder.AddConsole(); - //} - //foreach (var listener in Trace.Listeners.Cast().Where(l => !(l is DefaultTraceListener))) - //{ - // builder.AddTraceSource(new SourceSwitch(listener.Name, "All"), listener); - //} - }); - return services.AddIpc(); - } -} -public class HttpSysWebSocketsListener : IDisposable -{ - HttpListener _httpListener = new(); - public HttpSysWebSocketsListener(string uriPrefix) - { - _httpListener.Prefixes.Add(uriPrefix); - _httpListener.Start(); - } - public async Task Accept(CancellationToken token) - { - while (true) - { - var listenerContext = await _httpListener.GetContextAsync(); - if (listenerContext.Request.IsWebSocketRequest) - { - var webSocketContext = await listenerContext.AcceptWebSocketAsync(subProtocol: null); - return webSocketContext.WebSocket; - } - listenerContext.Response.StatusCode = 400; - listenerContext.Response.Close(); - } - } - public void Dispose() => _httpListener.Stop(); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs b/src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs deleted file mode 100644 index b63c41c6..00000000 --- a/src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Andrew Arnott. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace UiPath.CoreIpc.Tests; - -internal class OneWayStreamWrapper : Stream -{ - private readonly Stream innerStream; - private readonly bool canRead; - private readonly bool canWrite; - - internal OneWayStreamWrapper(Stream innerStream, bool canRead = false, bool canWrite = false) - { - if (canRead == canWrite) - { - throw new ArgumentException("Exactly one operation (read or write) must be true."); - } - this.innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); - this.canRead = canRead; - this.canWrite = canWrite; - } - - public override bool CanRead => this.canRead && this.innerStream.CanRead; - - public override bool CanSeek => false; - - public override bool CanWrite => this.canWrite && this.innerStream.CanWrite; - - public override long Length => throw new NotSupportedException(); - - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - public override void Flush() - { - if (this.CanWrite) - { - this.innerStream.Flush(); - } - else - { - throw new NotSupportedException(); - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - if (this.CanRead) - { - return this.innerStream.Read(buffer, offset, count); - } - else - { - throw new NotSupportedException(); - } - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (this.CanRead) - { - return this.innerStream.ReadAsync(buffer, offset, count, cancellationToken); - } - else - { - throw new NotSupportedException(); - } - } - - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) - { - if (this.CanWrite) - { - this.innerStream.Write(buffer, offset, count); - } - else - { - throw new NotSupportedException(); - } - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (this.CanWrite) - { - return this.innerStream.WriteAsync(buffer, offset, count, cancellationToken); - } - else - { - throw new NotSupportedException(); - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - this.innerStream.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs b/src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs deleted file mode 100644 index 049f4e36..00000000 --- a/src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Globalization; -using System.Text; - -namespace UiPath.CoreIpc.Tests; - -public interface ISystemService -{ - Task DoNothing(CancellationToken cancellationToken = default); - Task VoidThreadName(CancellationToken cancellationToken = default); - Task VoidSyncThrow(CancellationToken cancellationToken = default); - Task GetThreadName(CancellationToken cancellationToken = default); - Task ConvertText(string text, TextStyle style, CancellationToken cancellationToken = default); - Task ConvertTextWithArgs(ConvertTextArgs args, CancellationToken cancellationToken = default); - Task GetGuid(Guid guid, CancellationToken cancellationToken = default); - Task ReverseBytes(byte[] input, CancellationToken cancellationToken = default); - Task SlowOperation(CancellationToken cancellationToken = default); - Task MissingCallback(SystemMessage message, CancellationToken cancellationToken = default); - Task Infinite(CancellationToken cancellationToken = default); - Task ImpersonateCaller(Message message = null, CancellationToken cancellationToken = default); - Task SendMessage(SystemMessage message, CancellationToken cancellationToken = default); - Task Upload(Stream stream, int delay = 0, CancellationToken cancellationToken = default); - Task Download(string text, CancellationToken cancellationToken = default); - Task Echo(Stream input, CancellationToken cancellationToken = default); - Task UploadNoRead(Stream memoryStream, int delay = 0, CancellationToken cancellationToken = default); -} - -public class SystemMessage : Message -{ - public string Text { get; set; } - public int Delay { get; set; } -} -public class SystemService : ISystemService -{ - public SystemService() - { - } - - public async Task Infinite(CancellationToken cancellationToken = default) - { - await Task.Delay(Timeout.Infinite, cancellationToken); - return true; - } - public async Task ConvertTextWithArgs(ConvertTextArgs args, CancellationToken cancellationToken = default) - => await ConvertText(args.Text, args.TextStyle, cancellationToken); - - public async Task ConvertText(string text, TextStyle style, CancellationToken cancellationToken = default) - { - switch (style) - { - case TextStyle.TitleCase: - return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(text); - case TextStyle.Upper: - return CultureInfo.InvariantCulture.TextInfo.ToUpper(text); - default: - return text; - } - } - - public async Task SendMessage(SystemMessage message, CancellationToken cancellationToken = default) - { - var client = message.Client; - var callback = message.GetCallback(); - var clientId = await callback.GetId(message); - string returnValue = ""; - client.Impersonate(() => returnValue = client.GetUserName() + "_" + clientId + "_" + message.Text); - return returnValue; - } - - public bool DidNothing { get; set; } - - public async Task DoNothing(CancellationToken cancellationToken = default) - { - const int Timeout = -#if CI - 100; -#else - 10; -#endif - await Task.Delay(Timeout); - DidNothing = true; - } - - public async Task GetGuid(Guid guid, CancellationToken cancellationToken = default) - { - //throw new Exception("sssss"); - return guid; - } - - public async Task ReverseBytes(byte[] input, CancellationToken cancellationToken = default) - { - return input.Reverse().ToArray(); - } - - public async Task MissingCallback(SystemMessage message, CancellationToken cancellationToken = default) - { - if (message.Delay != 0) - { - await Task.Delay(message.Delay, cancellationToken); - } - var domainName = ""; - var client = message.Client; - //client.RunAs(() => domainName = "test"); - //try - //{ - message.GetCallback(); - //} - //catch(Exception ex) - //{ - // Console.WriteLine(ex.ToString()); - //} - return client.GetUserName() +" " + domainName; - } - - public async Task SlowOperation(CancellationToken cancellationToken = default) - { - Console.WriteLine("SlowOperation " + Thread.CurrentThread.Name); - try - { - for(int i = 0; i < 5; i++) - { - await Task.Delay(1000, cancellationToken); - Console.WriteLine("SlowOperation "+Thread.CurrentThread.Name); - if(cancellationToken.IsCancellationRequested) - { - Console.WriteLine("SlowOperation Cancelled."); - return false; - } - } - } - catch(Exception ex) - { - Console.WriteLine(ex.ToString()); - } - Console.WriteLine("SlowOperation finished. "+ (cancellationToken.IsCancellationRequested ? "cancelled " : "") + Thread.CurrentThread.Name); - return true; - } - - public string ThreadName; - - public Task VoidSyncThrow(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public async Task VoidThreadName(CancellationToken cancellationToken = default) => ThreadName = Thread.CurrentThread.Name; - - public async Task GetThreadName(CancellationToken cancellationToken = default) => Thread.CurrentThread.Name; - - public async Task ImpersonateCaller(Message message = null, CancellationToken cancellationToken = default) - { - var client = message.Client; - string returnValue = ""; - client.Impersonate(() => returnValue = client.GetUserName()); - return returnValue; - } - - public async Task Upload(Stream stream, int delay = 0, CancellationToken cancellationToken = default) - { - await Task.Delay(delay); - return await new StreamReader(stream).ReadToEndAsync(); - } - - public async Task UploadNoRead(Stream stream, int delay = 0, CancellationToken cancellationToken = default) - { - await Task.Delay(delay); - return ""; - } - - public async Task Download(string text, CancellationToken cancellationToken = default) => new MemoryStream(Encoding.UTF8.GetBytes(text)); - - public async Task Echo(Stream input, CancellationToken cancellationToken = default) - { - var result = new MemoryStream(); - await input.CopyToAsync(result); - result.Position = 0; - return result; - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs new file mode 100644 index 00000000..e43eee21 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs @@ -0,0 +1,76 @@ +using UiPath.Ipc.Transport.NamedPipe; + +namespace UiPath.Ipc.Tests; + +public sealed class NamedPipeSmokeTests +{ + [Fact] + public async Task NamedPipesShoulNotLeak() + { + var pipeName = $"ipctest_{Guid.NewGuid():N}"; + + (await ListPipes(pipeName)).ShouldBeNullOrEmpty(); + + await using (var ipcServer = CreateServer(pipeName)) + { + var ipcClient = CreateClient(pipeName); + var proxy = ipcClient.GetProxy(); + + await ipcServer.WaitForStart(); + await proxy.AddFloats(2, 3).ShouldBeAsync(5); + } + + (await ListPipes(pipeName)).ShouldBeNullOrEmpty(); + } + + private static IpcServer CreateServer(string pipeName) + => new IpcServer + { + Transport = new NamedPipeServerTransport + { + PipeName = pipeName, + }, + Endpoints = new() + { + typeof(IComputingService) + }, + ServiceProvider = new ServiceCollection() + .AddLogging() + .AddSingleton() + .BuildServiceProvider() + }; + + private static IpcClient CreateClient(string pipeName) + => new() + { + Transport = new NamedPipeClientTransport { PipeName = pipeName } + }; + + private static Task ListPipes(string pattern) + => RunPowershell($"(Get-ChildItem \\\\.\\pipe\\ | Select-Object FullName) | Where-Object {{ $_.FullName -like '{pattern}' }}"); + + private static async Task RunPowershell(string command) + { + var process = new Process + { + StartInfo = new() + { + FileName = "powershell", + Arguments = $"-Command \"{command}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + var output = await process.StandardOutput.ReadToEndAsync(); + var error = await process.StandardError.ReadToEndAsync(); + await Task.Run(process.WaitForExit); + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"Failed to run powershell. ExitCode: {process.ExitCode}. Error: {error}"); + } + return output; + } +} diff --git a/src/UiPath.CoreIpc.Tests/NamedPipeTests.cs b/src/UiPath.CoreIpc.Tests/NamedPipeTests.cs deleted file mode 100644 index e81f1eaa..00000000 --- a/src/UiPath.CoreIpc.Tests/NamedPipeTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.IO.Pipes; -using System.Security.Principal; -namespace UiPath.CoreIpc.Tests; - -public class SystemNamedPipeTests : SystemTests> -{ - string _pipeName = "system"; - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => - serviceHostBuilder.UseNamedPipes(Configure(new NamedPipeSettings(_pipeName+GetHashCode()))); - protected override NamedPipeClientBuilder CreateSystemClientBuilder() => - new NamedPipeClientBuilder(_pipeName+GetHashCode()).AllowImpersonation(); - [Fact] - public void PipeExists() - { - IOHelpers.PipeExists(System.Guid.NewGuid().ToString()).ShouldBeFalse(); - IOHelpers.PipeExists("system"+GetHashCode(), 50).ShouldBeTrue(); - } - [Fact] - public Task ServerName() => SystemClientBuilder().ValidateAndBuild().GetGuid(System.Guid.Empty); - [Fact] - public override void BeforeCallServerSide() - { - _pipeName = "beforeCall"; - base.BeforeCallServerSide(); - } -#if WINDOWS - [Fact] - public async Task PipeSecurityForWindows() - { - _pipeName = "protected"; - using var protectedService = new ServiceHostBuilder(_serviceProvider) - .UseNamedPipes(Configure(new NamedPipeSettings(_pipeName+GetHashCode()) - { - AccessControl = pipeSecurity => pipeSecurity.Deny(WellKnownSidType.WorldSid, PipeAccessRights.FullControl) - })) - .AddEndpoint() - .ValidateAndBuild(); - _ = protectedService.RunAsync(); - await CreateSystemService().DoNothing().ShouldThrowAsync(); - } -#endif -} -public class ComputingNamedPipeTests : ComputingTests> -{ - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => - serviceHostBuilder.UseNamedPipes(Configure(new NamedPipeSettings("computing" + GetHashCode()))); - protected override NamedPipeClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => - new NamedPipeClientBuilder("computing" + GetHashCode(), _serviceProvider) - .AllowImpersonation() - .RequestTimeout(RequestTimeout) - .CallbackInstance(_computingCallback) - .TaskScheduler(taskScheduler); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/NestedStreamTests.cs b/src/UiPath.CoreIpc.Tests/NestedStreamTests.cs deleted file mode 100644 index b30d6f93..00000000 --- a/src/UiPath.CoreIpc.Tests/NestedStreamTests.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System.IO.Compression; - -namespace UiPath.CoreIpc.Tests; - -public class NestedStreamTests -{ - private const int DefaultNestedLength = 10; - - private MemoryStream underlyingStream; - - private NestedStream stream; - - protected static readonly TimeSpan UnexpectedTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); - - private readonly CancellationTokenSource _timeoutTokenSource = new(UnexpectedTimeout); - - public NestedStreamTests() - { - var random = new Random(); - var buffer = new byte[20]; - random.NextBytes(buffer); - this.underlyingStream = new MemoryStream(buffer); - this.stream = this.underlyingStream.ReadSlice(DefaultNestedLength); - } - - protected CancellationToken TimeoutToken => Debugger.IsAttached ? CancellationToken.None : _timeoutTokenSource.Token; - - [Fact] - public void CanSeek() - { - Assert.True(this.stream.CanSeek); - this.stream.Dispose(); - Assert.False(this.stream.CanSeek); - } - - [Fact] - public void CanSeek_NonSeekableStream() - { - using var gzipStream = new GZipStream(Stream.Null, CompressionMode.Decompress); - using var stream = gzipStream.ReadSlice(10); - - Assert.False(stream.CanSeek); - stream.Dispose(); - Assert.False(stream.CanSeek); - } - - [Fact] - public void Length() - { - Assert.Equal(DefaultNestedLength, this.stream.Length); - } - - [Fact] - public void Length_NonSeekableStream() - { - using (var gzipStream = new GZipStream(Stream.Null, CompressionMode.Decompress)) - using (var stream = gzipStream.ReadSlice(10)) - { - stream.Length.ShouldBe(10); - } - } - - [Fact] - public void Position() - { - byte[] buffer = new byte[DefaultNestedLength]; - - Assert.Equal(0, this.stream.Position); - var bytesRead = this.stream.Read(buffer, 0, 5); - Assert.Equal(bytesRead, this.stream.Position); - - this.stream.Position = 0; - byte[] buffer2 = new byte[DefaultNestedLength]; - bytesRead = this.stream.Read(buffer2, 0, 5); - Assert.Equal(bytesRead, this.stream.Position); - Assert.Equal(buffer, buffer2); - } - - [Fact] - public void Position_NonSeekableStream() - { - using var nonSeekableWrapper = new OneWayStreamWrapper(this.underlyingStream, canRead: true); - using var stream = nonSeekableWrapper.ReadSlice(10); - - Assert.Equal(0, stream.Position); - Assert.Throws(() => stream.Position = 3); - Assert.Equal(0, stream.Position); - stream.ReadByte(); - Assert.Equal(1, stream.Position); - } - - [Fact] - public void IsDisposed() - { - Assert.False(stream.IsDisposed); - this.stream.Dispose(); - Assert.True(stream.IsDisposed); - } - - [Fact] - public void Dispose_IncompleteDisposesUnderylingStream() - { - this.stream.Dispose(); - Assert.False(this.underlyingStream.CanSeek); - } - - [Fact] - public void Dispose_DoesNotDisposeUnderylingStream() - { - this.stream.Read(new byte[DefaultNestedLength], 0, DefaultNestedLength); - this.stream.Dispose(); - Assert.True(this.underlyingStream.CanSeek); - // A sanity check that if it were disposed, our assertion above would fail. - this.underlyingStream.Dispose(); - Assert.False(this.underlyingStream.CanSeek); - } - - [Fact] - public void SetLength() - { - Assert.Throws(() => this.stream.SetLength(0)); - } - - [Fact] - public void Seek_Current() - { - Assert.Equal(0, this.stream.Position); - Assert.Equal(0, this.stream.Seek(0, SeekOrigin.Current)); - Assert.Equal(0, this.underlyingStream.Position); - Assert.Throws(() => this.stream.Seek(-1, SeekOrigin.Current)); - Assert.Equal(0, this.underlyingStream.Position); - - Assert.Equal(5, this.stream.Seek(5, SeekOrigin.Current)); - Assert.Equal(5, this.underlyingStream.Position); - Assert.Equal(5, this.stream.Seek(0, SeekOrigin.Current)); - Assert.Equal(5, this.underlyingStream.Position); - Assert.Equal(4, this.stream.Seek(-1, SeekOrigin.Current)); - Assert.Equal(4, this.underlyingStream.Position); - Assert.Throws(() => this.stream.Seek(-10, SeekOrigin.Current)); - Assert.Equal(4, this.underlyingStream.Position); - - Assert.Equal(0, this.stream.Seek(0, SeekOrigin.Begin)); - Assert.Equal(0, this.stream.Position); - - Assert.Equal(DefaultNestedLength + 1, this.stream.Seek(DefaultNestedLength + 1, SeekOrigin.Current)); - Assert.Equal(DefaultNestedLength + 1, this.underlyingStream.Position); - Assert.Equal((2 * DefaultNestedLength) + 1, this.stream.Seek(DefaultNestedLength, SeekOrigin.Current)); - Assert.Equal((2 * DefaultNestedLength) + 1, this.underlyingStream.Position); - Assert.Equal((2 * DefaultNestedLength) + 1, this.stream.Seek(0, SeekOrigin.Current)); - Assert.Equal((2 * DefaultNestedLength) + 1, this.underlyingStream.Position); - Assert.Equal(1, this.stream.Seek(-2 * DefaultNestedLength, SeekOrigin.Current)); - Assert.Equal(1, this.underlyingStream.Position); - - this.stream.Dispose(); - Assert.Throws(() => this.stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void Sook_WithNonStartPositionInUnderlyingStream() - { - this.underlyingStream.Position = 1; - this.stream = this.underlyingStream.ReadSlice(5); - - Assert.Equal(0, this.stream.Position); - Assert.Equal(2, this.stream.Seek(2, SeekOrigin.Current)); - Assert.Equal(3, this.underlyingStream.Position); - } - - [Fact] - public void Seek_Begin() - { - Assert.Equal(0, this.stream.Position); - Assert.Throws(() => this.stream.Seek(-1, SeekOrigin.Begin)); - Assert.Equal(0, this.underlyingStream.Position); - - Assert.Equal(0, this.stream.Seek(0, SeekOrigin.Begin)); - Assert.Equal(0, this.underlyingStream.Position); - - Assert.Equal(5, this.stream.Seek(5, SeekOrigin.Begin)); - Assert.Equal(5, this.underlyingStream.Position); - - Assert.Equal(DefaultNestedLength, this.stream.Seek(DefaultNestedLength, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - - Assert.Equal(DefaultNestedLength + 1, this.stream.Seek(DefaultNestedLength + 1, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength + 1, this.underlyingStream.Position); - - this.stream.Dispose(); - Assert.Throws(() => this.stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void Seek_End() - { - Assert.Equal(0, this.stream.Position); - Assert.Equal(9, this.stream.Seek(-1, SeekOrigin.End)); - Assert.Equal(9, this.underlyingStream.Position); - - Assert.Equal(DefaultNestedLength, this.stream.Seek(0, SeekOrigin.End)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - - Assert.Equal(DefaultNestedLength + 5, this.stream.Seek(5, SeekOrigin.End)); - Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); - - Assert.Throws(() => this.stream.Seek(-20, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); - - this.stream.Dispose(); - Assert.Throws(() => this.stream.Seek(0, SeekOrigin.End)); - } - - [Fact] - public void Flush() - { - Assert.Throws(() => this.stream.Flush()); - } - - [Fact] - public async Task FlushAsync() - { - await Assert.ThrowsAsync(() => this.stream.FlushAsync()); - } - - [Fact] - public void CanRead() - { - Assert.True(this.stream.CanRead); - this.stream.Dispose(); - Assert.False(this.stream.CanRead); - } - - [Fact] - public void CanWrite() - { - Assert.False(this.stream.CanWrite); - this.stream.Dispose(); - Assert.False(this.stream.CanWrite); - } - - [Fact] - public async Task WriteAsync_Throws() - { - await Assert.ThrowsAsync(() => this.stream.WriteAsync(new byte[1], 0, 1)); - } - - [Fact] - public void Write_Throws() - { - Assert.Throws(() => this.stream.Write(new byte[1], 0, 1)); - } - - [Fact] - public async Task ReadAsync_Empty_ReturnsZero() - { - Assert.Equal(0, await this.stream.ReadAsync(Array.Empty(), 0, 0, default)); - } - - [Fact] - public async Task Read_BeyondEndOfStream_ReturnsZero() - { - // Seek beyond the end of the stream - this.stream.Seek(1, SeekOrigin.End); - - byte[] buffer = new byte[this.underlyingStream.Length]; - - Assert.Equal(0, await this.stream.ReadAsync(buffer, 0, buffer.Length, this.TimeoutToken)); - } - - [Fact] - public async Task ReadAsync_NoMoreThanGiven() - { - byte[] buffer = new byte[this.underlyingStream.Length]; - int bytesRead = await this.stream.ReadAsync(buffer, 0, buffer.Length, this.TimeoutToken); - Assert.Equal(DefaultNestedLength, bytesRead); - - Assert.Equal(0, await this.stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead, this.TimeoutToken)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - } - - [Fact] - public void Read_NoMoreThanGiven() - { - byte[] buffer = new byte[this.underlyingStream.Length]; - int bytesRead = this.stream.Read(buffer, 0, buffer.Length); - Assert.Equal(DefaultNestedLength, bytesRead); - - Assert.Equal(0, this.stream.Read(buffer, bytesRead, buffer.Length - bytesRead)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - } - - [Fact] - public void Read_Empty_ReturnsZero() - { - Assert.Equal(0, this.stream.Read(Array.Empty(), 0, 0)); - } - - [Fact] - public async Task ReadAsync_WhenLengthIsInitially0() - { - this.stream = this.underlyingStream.ReadSlice(0); - Assert.Equal(0, await this.stream.ReadAsync(new byte[1], 0, 1, this.TimeoutToken)); - } - - [Fact] - public void Read_WhenLengthIsInitially0() - { - this.stream = this.underlyingStream.ReadSlice(0); - Assert.Equal(0, this.stream.Read(new byte[1], 0, 1)); - } - - [Fact] - public void CreationDoesNotReadFromUnderlyingStream() - { - Assert.Equal(0, this.underlyingStream.Position); - } - - [Fact] - public void Read_UnderlyingStreamReturnsFewerBytesThanRequested() - { - var buffer = new byte[20]; - int firstBlockLength = DefaultNestedLength / 2; - this.underlyingStream.SetLength(firstBlockLength); - Assert.Equal(firstBlockLength, this.stream.Read(buffer, 0, buffer.Length)); - this.underlyingStream.SetLength(DefaultNestedLength * 2); - Assert.Equal(DefaultNestedLength - firstBlockLength, this.stream.Read(buffer, 0, buffer.Length)); - } - - [Fact] - public async Task ReadAsync_UnderlyingStreamReturnsFewerBytesThanRequested() - { - var buffer = new byte[20]; - int firstBlockLength = DefaultNestedLength / 2; - this.underlyingStream.SetLength(firstBlockLength); - Assert.Equal(firstBlockLength, await this.stream.ReadAsync(buffer, 0, buffer.Length)); - this.underlyingStream.SetLength(DefaultNestedLength * 2); - Assert.Equal(DefaultNestedLength - firstBlockLength, await this.stream.ReadAsync(buffer, 0, buffer.Length)); - } - - [Fact] - public void Read_ValidatesArguments() - { - var buffer = new byte[20]; - - Assert.Throws(() => this.stream.Read(null!, 0, 0)); - Assert.Throws(() => this.stream.Read(buffer, -1, buffer.Length)); - Assert.Throws(() => this.stream.Read(buffer, 0, -1)); - Assert.Throws(() => this.stream.Read(buffer, 1, buffer.Length)); - } - - [Fact] - public async Task ReadAsync_ValidatesArguments() - { - var buffer = new byte[20]; - - await Assert.ThrowsAsync(() => this.stream.ReadAsync(null!, 0, 0)); - await Assert.ThrowsAsync(() => this.stream.ReadAsync(buffer, -1, buffer.Length)); - await Assert.ThrowsAsync(() => this.stream.ReadAsync(buffer, 0, -1)); - await Assert.ThrowsAsync(() => this.stream.ReadAsync(buffer, 1, buffer.Length)); - } -} -public static class StreamExtensions -{ - /// - /// Creates a that can read no more than a given number of bytes from an underlying stream. - /// - /// The stream to read from. - /// The number of bytes to read from the parent stream. - /// A stream that ends after bytes are read. - public static NestedStream ReadSlice(this Stream stream, long length) => new(stream, length); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.CoreIpc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..0032e3dd --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,36 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +// +// Summary: +// Allows capturing of the expressions passed to a method. +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + // + // Summary: + // Initializes a new instance of the System.Runtime.CompilerServices.CallerArgumentExpressionAttribute + // class. + // + // Parameters: + // parameterName: + // The name of the targeted parameter. + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + // + // Summary: + // Gets the target parameter name of the CallerArgumentExpression. + // + // Returns: + // The name of the targeted parameter of the CallerArgumentExpression. + public string ParameterName { get; } +} + + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Polyfills/IsExternalInit.cs b/src/UiPath.CoreIpc.Tests/Polyfills/IsExternalInit.cs new file mode 100644 index 00000000..dc521fbf --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Polyfills/IsExternalInit.cs @@ -0,0 +1,19 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +/// +/// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal static class IsExternalInit +{ +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Polyfills/System_Index.cs b/src/UiPath.CoreIpc.Tests/Polyfills/System_Index.cs new file mode 100644 index 00000000..177b155c --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Polyfills/System_Index.cs @@ -0,0 +1,151 @@ +// +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal readonly struct Index : IEquatable +{ + readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (fromEnd) + { + _value = ~value; + } + else + { + _value = value; + } + } + + // The following private constructors mainly created for perf reason to avoid the checks + Index(int value) => + _value = value; + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object value) => value is Index index && _value == index._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + { + return ToStringFromEnd(); + } + + return ((uint)Value).ToString(); + } + + string ToStringFromEnd() => + '^' + Value.ToString(); +} +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Program.cs b/src/UiPath.CoreIpc.Tests/Program.cs new file mode 100644 index 00000000..73419438 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Program.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Text; + +namespace UiPath.Ipc.Tests; + +internal static class Program +{ + public static async Task Main(string[] args) + { + using (ConsoleCancellation(out var ct)) + { + return await Entry(args, ct); + } + } + + private static async Task Entry(string[] args, CancellationToken ct) + { + if (args is not [var base64]) + { + Console.Error.WriteLine($"Usage: dotnet {Path.GetFileName(Assembly.GetEntryAssembly()!.Location)} "); + return 1; + } + var externalServerParams = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(base64))); + await using var asyncDisposable = externalServerParams.CreateListenerConfig(out var serverTransport); + + await using var serviceProvider = new ServiceCollection() + .AddLogging(builder => builder.AddConsole()) + .AddSingleton() + .BuildServiceProvider(); + + await using var ipcServer = new IpcServer() + { + ServiceProvider = serviceProvider, + Scheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler, + Endpoints = new() + { + { typeof(IComputingService) }, + }, + Transport = serverTransport, + }; + ipcServer.Start(); + await Task.Delay(Timeout.InfiniteTimeSpan, ct); + + return 0; + } + + private static IDisposable ConsoleCancellation(out CancellationToken ct) + { + var cts = new CancellationTokenSource(); + ct = cts.Token; + Console.CancelKeyPress += (sender, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + return cts; + } +} + diff --git a/src/UiPath.CoreIpc.Tests/RobotTests.cs b/src/UiPath.CoreIpc.Tests/RobotTests.cs new file mode 100644 index 00000000..bc173e51 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/RobotTests.cs @@ -0,0 +1,59 @@ +using NSubstitute; +using System.Collections.Concurrent; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public abstract class RobotTests : TestBase +{ + #region " Setup " + protected readonly StudioEvents _studioEvents = new(); + + private readonly Lazy _service; + private readonly Lazy _proxy; + + protected StudioOperations Service => _service.Value; + protected IStudioOperations Proxy => _proxy.Value!; + + protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; + protected sealed override Type ContractType => typeof(IStudioOperations); + protected override ContractCollection? Callbacks => new() + { + { typeof(IStudioEvents), _studioEvents } + }; + + protected readonly ConcurrentBag _clientBeforeCalls = new(); + + protected RobotTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + ServiceProvider.InjectLazy(out _service); + CreateLazyProxy(out _proxy); + } + + protected override void ConfigureSpecificServices(IServiceCollection services) + => services + .AddSingleton() + .AddSingletonAlias(); + + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; + #endregion + + [Fact] + public async Task StudioEvents_ShouldWork() + { + var spy = Substitute.For(); + using var spyInstallation = _studioEvents.RouteTo(spy); + + await Proxy.SetOffline(true); + await spy.ReceivedWithAnyArgs(0).OnRobotInfoChanged(Arg.Any()); + + var info = await GetProxy()!.GetRobotInfoCore(message: new()); + await spy.ReceivedWithAnyArgs(0).OnRobotInfoChanged(Arg.Any()); + + await Proxy.SetOffline(false); + await spy.Received(1).OnRobotInfoChanged(Arg.Is(x => !x.LatestInfo.Offline)); + + await Proxy.SetOffline(true); + await spy.Received(1).OnRobotInfoChanged(Arg.Is(x => x.LatestInfo.Offline)); + } +} diff --git a/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs new file mode 100644 index 00000000..95397289 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs @@ -0,0 +1,276 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using UiPath.Ipc.Transport.NamedPipe; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public sealed class RobotTestsOverNamedPipes : RobotTests +{ + private string PipeName => Names.GetPipeName(role: "robot", TestRunId); + + public RobotTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected override async Task CreateServerTransport() => new NamedPipeServerTransport + { + PipeName = PipeName + }; + protected override ClientTransport CreateClientTransport() => new NamedPipeClientTransport + { + PipeName = PipeName, + AllowImpersonation = true, + }; + + + [Fact] + public async Task CommandLineTest() + { + var lazyProxy = new Lazy(() => StudioOperationsProxyFactory.Create(PipeName)); + await lazyProxy.Value.GetRobotInfoCore(message: new()); + await lazyProxy.Value.SetOffline(true); + } + + private static class StudioOperationsProxyFactory + { + public static IStudioOperations Create(string pipeName, IStudioEvents events = null!) => Communication.GivenCallback(events ?? EmptyStudioEvents.Instance).CreateUserServiceProxy(pipeName); + + class EmptyStudioEvents : IStudioEvents + { + public static readonly EmptyStudioEvents Instance = new(); + + public Task OnRobotInfoChanged(RobotInfoChangedArgs args) => Task.CompletedTask; + } + } + + private static class Communication + { + public static CallbackInstance GivenCallback(TCallback callback) where TCallback : class => new(callback); + + public static void OnConnectingToUserService() + { + // do nothing + } + } + + public readonly struct CallbackInstance where TCallback : class + { + public TCallback Instance { get; } + + public CallbackInstance(TCallback instance) => Instance = instance; + + public TContract CreateUserServiceProxy(string pipeName) + where TContract : class + => RobotIpcHelpers.CreateProxy( + pipeName, + requestTimeout: TimeSpan.FromSeconds(40), + callbacks: new ContractCollection() + { + { typeof(TCallback), Instance } + }, + beforeConnect: Communication.OnConnectingToUserService); + } +} + +internal static partial class RobotIpcHelpers +{ + private static readonly ConcurrentDictionary PipeClients = new(); + + public static TContract CreateProxy( + string pipeName, + TimeSpan? requestTimeout = null, + ContractCollection? callbacks = null, + IServiceProvider? provider = null, + Action? beforeConnect = null, + BeforeCallHandler? beforeCall = null, + bool allowImpersonation = false, + TaskScheduler? scheduler = null) where TContract : class + { + // TODO: Fix this + // Dirty hack (temporary): different callback sets will result in different connections + // Hopefully, the different sets are also disjunctive. + + // We're still making sure that beforeConnect, beforeCall, scheduler and requestTimeout are the same. + // If that happens, and exception will be thrown. + + // If the sets are indeed disjunctive, and they should, since the original API provided on callback type per service type, + // then the server will not erroneously inflate the number of client callbacks. + + // What might happen invisibly is that beforeConnect will be called more than once, but that's hopefully idempotent. + var actualKey = new Key( + pipeName, + allowImpersonation, + EquatableEndpointSet.From(callbacks, haveProvider: provider is not null)); + + Params requestedParams = new( + requestTimeout, + provider, + scheduler, + beforeCall, + beforeConnect); + + var (client, originalParams) = PipeClients.GetOrAdd( + new(actualKey, requestedParams, callbacks), + CreateClient); + + if (requestedParams - originalParams is { } ex) + { + throw ex; + } + + return client.GetProxy(); + } + + private static ClientAndParams CreateClient(CreateProxyRequest request) + => new( + Client: new() + { + RequestTimeout = request.Params.RequestTimeout ?? Timeout.InfiniteTimeSpan, + ServiceProvider = request.Params.Provider, + Logger = request.Params.Provider?.GetService>(), + Callbacks = request.Callbacks, + BeforeConnect = request.Params.BeforeConnect is null ? null : _ => + { + request.Params.BeforeConnect(); + return Task.CompletedTask; + }, + BeforeOutgoingCall = request.Params.BeforeCall, + Scheduler = request.Params.Scheduler, + Transport = new NamedPipeClientTransport + { + PipeName = request.ActualKey.Name, + AllowImpersonation = request.ActualKey.AllowImpersonation, + }, + }, + request.Params); + + internal readonly record struct Key(string Name, bool AllowImpersonation, EquatableEndpointSet Callbacks); + internal readonly record struct Params( + TimeSpan? RequestTimeout, + IServiceProvider? Provider, + TaskScheduler? Scheduler, + BeforeCallHandler? BeforeCall, + Action? BeforeConnect) + { + public static Exception? operator -(Params @new, Params old) + { + var differences = EnumerateDifferences().ToArray(); + if (differences.Length is 0) return null; + return new InvalidOperationException($"{nameof(Params)} differences:\r\n{string.Join("\r\n", differences)}"); + + IEnumerable EnumerateDifferences() + { + if (@new.RequestTimeout != old.RequestTimeout) + { + yield return Compose(nameof(RequestTimeout), @new.RequestTimeout, old.RequestTimeout); + } + if (AreDifferent(@new.Provider, old.Provider)) + { + yield return Compose(nameof(Provider), @new.Provider, old.Provider); + } + if (AreDifferent(@new.Scheduler, old.Scheduler)) + { + yield return Compose(nameof(Scheduler), @new.Scheduler, old.Scheduler); + } + if (AreDifferent(@new.BeforeCall, old.BeforeCall)) + { + yield return Compose(nameof(BeforeCall), @new.BeforeCall, old.BeforeCall); + } + if (AreDifferent(@new.BeforeConnect, old.BeforeConnect)) + { + yield return Compose(nameof(BeforeConnect), @new.BeforeConnect, old.BeforeConnect); + } + } + + static bool AreDifferent(T? @new, T? old) where T : class + { + if (@new is null && old is null) + { + return false; + } + if (@new is null || old is null) + { + return true; + } + return !@new.Equals(old); + } + + static string Compose(string name, T @new, T old) + => $"New {name} is {@new?.ToString() ?? "null"} but was originally {old?.ToString() ?? "null"}."; + } + } + internal readonly record struct CreateProxyRequest(Key ActualKey, Params Params, ContractCollection? Callbacks) + { + public bool Equals(CreateProxyRequest other) => ActualKey.Equals(other.ActualKey); + public override int GetHashCode() => ActualKey.GetHashCode(); + } + internal readonly record struct ClientAndParams(IpcClient Client, Params Params); + + internal readonly struct EquatableEndpointSet : IEquatable + { + public static EquatableEndpointSet From(ContractCollection? endpoints, bool haveProvider) + { + return Pal(endpoints?.AsEnumerable(), haveProvider); + + static EquatableEndpointSet Pal(IEnumerable? endpoints, bool haveProvider) + { + var items = endpoints?.AsEnumerable(); + + // Dirty fix (temporary): + // Reduce the chance of difference callback sets by removing null callback instances when there'n no service provider. + // If the Robot's maturity says anything about such situations it that the server will not use those callbacks. + if (!haveProvider) + { + items = items?.Where(callback => callback.ServiceInstance is not null); + } + + var set = items?.ToHashSet(); + + // reuse the cached empty set for null callback sets, empty callback sets or non-empty callback sets that result in being empty after the removal. + if (set is not { Count: > 0 }) + { + return Empty; + } + + return new(set); + } + } + + public static readonly EquatableEndpointSet Empty = new([]); + private readonly HashSet _set; + + private EquatableEndpointSet(HashSet set) => _set = set; + + public bool Equals(EquatableEndpointSet other) => _set.SetEquals(other._set); + public override bool Equals(object? obj) => obj is EquatableEndpointSet other && Equals(other); + public override int GetHashCode() => _set.Count; + + public override string ToString() + { + return $"[{string.Join(", ", _set.Select(Pal))}]"; + static string Pal(ContractSettings endpointSettings) + => $"{endpointSettings.ContractType.Name},sp:{RuntimeHelpers.GetHashCode(endpointSettings.ServiceProvider)},instance:{RuntimeHelpers.GetHashCode(endpointSettings.ServiceInstance)}"; + } + } + +} + +internal static class HashSetExtensions +{ + public static HashSet ToHashSet(this IEnumerable source) => source.ToHashSet(null); + + public static HashSet ToHashSet(this IEnumerable source, IEqualityComparer? comparer) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source is HashSet existingHashSet) + { + return existingHashSet; + } + + return new HashSet(source, comparer); + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs new file mode 100644 index 00000000..4493a934 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs @@ -0,0 +1,7 @@ +namespace UiPath.Ipc.Tests; + +public sealed class ArithmeticCallback : IArithmeticCallback +{ + public async Task Increment(int x) => x + 1; +} + diff --git a/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs new file mode 100644 index 00000000..630a8c71 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs @@ -0,0 +1,11 @@ +namespace UiPath.Ipc.Tests; + +public sealed class ComputingCallback : IComputingCallback +{ + public Guid Id { get; } = Guid.NewGuid(); + + public async Task GetThreadName() => Thread.CurrentThread.Name!; + + public async Task AddInts(int x, int y) => x + y; +} + diff --git a/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs new file mode 100644 index 00000000..90d7994e --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Logging; + +namespace UiPath.Ipc.Tests; + +public sealed class ComputingService(ILogger logger) : IComputingService +{ + private static readonly AsyncLocal ContextStorage = new(); + public static string? Context + { + get => ContextStorage.Value; + set => ContextStorage.Value = value; + } + + public async Task AddFloats(float a, float b, CancellationToken ct = default) + { + logger.LogInformation($"{nameof(AddFloats)} called."); + return a + b; + } + + public async Task AddComplexNumbers(ComplexNumber a, ComplexNumber b, CancellationToken ct = default) + { + logger.LogInformation($"{nameof(AddComplexNumbers)} called."); + return a + b; + } + public async Task AddComplexNumberList(IReadOnlyList numbers, CancellationToken ct) + { + var result = ComplexNumber.Zero; + foreach (var number in numbers) + { + result += number; + } + return result; + } + + public async Task Wait(TimeSpan duration, CancellationToken ct = default) + { + await Task.Delay(duration, ct); + return true; + } + + public async Task GetCallbackThreadName(TimeSpan waitOnServer, Message message = null!, CancellationToken cancellationToken = default) + { + await Task.Delay(waitOnServer); + return await message.Client.GetCallback().GetThreadName(); + } + + public async Task MultiplyInts(int x, int y, Message message = null!) + { + var callback = message.Client.GetCallback(); + + var result = 0; + for (int i = 0; i < y; i++) + { + result = await callback.AddInts(result, x); + } + + return result; + } + + public async Task GetCallContext() + { + await Task.Delay(1).ConfigureAwait(continueOnCapturedContext: false); + return Context; + } + + public async Task SendMessage(Message m = null!, CancellationToken ct = default) + { + return await m.Client.GetCallback().GetThreadName(); + } +} diff --git a/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs new file mode 100644 index 00000000..8bfe8e00 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs @@ -0,0 +1,49 @@ + +namespace UiPath.Ipc.Tests; + +public interface IComputingServiceBase +{ + Task AddFloats(float x, float y, CancellationToken ct = default); +} + +public interface IComputingService : IComputingServiceBase +{ + Task AddComplexNumbers(ComplexNumber a, ComplexNumber b, CancellationToken ct = default); + Task AddComplexNumberList(IReadOnlyList numbers, CancellationToken ct = default); + Task Wait(TimeSpan duration, CancellationToken ct = default); + Task GetCallbackThreadName(TimeSpan waitOnServer, Message message = null!, CancellationToken cancellationToken = default); + Task MultiplyInts(int x, int y, Message message = null!); + Task GetCallContext(); + Task SendMessage(Message m = null!, CancellationToken ct = default); +} + +public interface IComputingCallbackBase +{ + Task AddInts(int x, int y); +} + +public interface IComputingCallback : IComputingCallbackBase +{ + Task GetThreadName(); +} + +public interface IArithmeticCallback +{ + Task Increment(int x); +} + +public readonly record struct ComplexNumber +{ + public static readonly ComplexNumber Zero = default; + public static ComplexNumber operator +(ComplexNumber a, ComplexNumber b) + => new() + { + I = a.I + b.I, + J = a.J + b.J + }; + + public required float I { get; init; } + public required float J { get; init; } + + public override string ToString() => $"[{I}, {J}]"; +} diff --git a/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs new file mode 100644 index 00000000..8fd915ee --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs @@ -0,0 +1,55 @@ +namespace UiPath.Ipc.Tests; + +public interface ISystemService +{ + /// + /// Returns the after the is ellapsed. + /// + /// The duration to wait before completing the operation. + /// A to cancel the operation. + /// A task that completes successfully with a null result, after the specified , or is canceled when the passed is signaled. + Task EchoGuidAfter(Guid value, TimeSpan waitOnServer, Message? message = null, CancellationToken ct = default); + + /// + /// Returns true if the received is not null. + /// + /// An optional . + /// + Task MessageReceivedAsNotNull(Message? message = null); + + /// + /// A method that does not return a result and whose algorithm will not be awaited by the remote client. + /// + /// A task that completes when the Ipc infrastructure confirms that the operation has begun but way before it has ended. + Task FireAndForget(TimeSpan wait); + + /// + /// A method that does not return a result and whose algorithm will not be awaited by the remote client. + /// + /// A task that completes when the Ipc infrastructure confirms that the operation has begun but way before it has ended. + Task FireAndForgetWithCt(CancellationToken ct); + + Task EchoString(string value); + + Task<(string ExceptionType, string ExceptionMessage, string? MarshalledExceptionType)?> CallUnregisteredCallback(Message message = null!); + + Task FireAndForgetThrowSync(); + + Task GetThreadName(); + + Task UploadEcho(Stream stream, CancellationToken ct = default); + + Task UploadJustCountBytes(Stream stream, int serverReadByteCount, TimeSpan serverDelay, CancellationToken ct = default); + Task Download(string s, CancellationToken ct = default); + + Task AddIncrement(int x, int y, Message message = null!); + + Task DanishNameOfDay(DayOfWeek day, CancellationToken ct); + + Task ReverseBytes(byte[] bytes, CancellationToken ct = default); +} + +public interface IUnregisteredCallback +{ + Task SomeMethod(); +} diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs new file mode 100644 index 00000000..2469b06a --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs @@ -0,0 +1,36 @@ +namespace UiPath.Ipc.Tests; + +public interface IStudioOperations : IStudioAgentOperations +{ + Task SetOffline(bool value); +} + +public interface IStudioAgentOperations +{ + Task GetRobotInfoCore(StudioAgentMessage message, CancellationToken ct = default); +} + +public interface IStudioEvents +{ + Task OnRobotInfoChanged(RobotInfoChangedArgs args); +} + +public class RobotInfo +{ + public bool Offline { get; set; } +} + +public sealed class RobotInfoChangedArgs +{ + public required RobotInfo LatestInfo { get; init; } +} + +public class StudioAgentMessage : ClientProcessMessage +{ + public Guid MasterJobId { get; set; } +} + +public class ClientProcessMessage : Message +{ + public int MockClientPid { get; set; } = 123; +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs new file mode 100644 index 00000000..f02fdea9 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs @@ -0,0 +1,72 @@ +using Nito.AsyncEx; +using Nito.Disposables; + +namespace UiPath.Ipc.Tests; + +public class StudioOperations : IStudioOperations +{ + private readonly Callbacks _studio = new Callbacks(); + + private readonly AsyncLock _lock = new(); + private readonly RobotInfo _latestInfo = new() { Offline = false }; + + public async Task GetRobotInfoCore(StudioAgentMessage message, CancellationToken ct = default) + { + using (await _lock.LockAsync()) + { + _studio.TryRegister(message, out var callback); + return _latestInfo; + } + } + + public async Task SetOffline(bool value) + { + using (await _lock.LockAsync()) + { + _latestInfo.Offline = value; + await _studio.InvokeAsync(async callback => await callback.OnRobotInfoChanged(new RobotInfoChangedArgs { LatestInfo = _latestInfo })); + } + + return true; + } +} + +public class StudioEvents : IStudioEvents +{ + private readonly object _lock = new(); + private IStudioEvents? _target = null; + + public IDisposable RouteTo(IStudioEvents? target) + { + lock (_lock) + { + var previousTarget = _target; + _target = target; + return new Disposable(() => + { + lock (_lock) + { + _target = previousTarget; + } + }); + } + } + + private IStudioEvents? GetTarget() + { + lock (_lock) + { + return _target; + } + } + + public async Task OnRobotInfoChanged(RobotInfoChangedArgs args) + { + if (GetTarget() is not { } target) + { + return; + } + + await target.OnRobotInfoChanged(args); + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs new file mode 100644 index 00000000..8d832555 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs @@ -0,0 +1,100 @@ +using Nito.AsyncEx; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace UiPath.Ipc.Tests; + +internal sealed class Callbacks where T : class +{ + private readonly List> _callbacks = new List>(); + + public bool Any() => _callbacks.Count != 0; + + public bool TryRegister(Message message, out Callback callback) // false if already registered + { + var existingCallback = _callbacks.FirstOrDefault(c => c.Client == message.Client); + if (existingCallback is not null) + { + callback = existingCallback; + return false; + } + + callback = new Callback(message); + _callbacks.Add(callback); + Trace.TraceInformation($"{nameof(Callbacks)}: Client {callback.GetHashCode()} added"); + return true; + } + + public void Invoke(Func call) => InvokeAsync(call).TraceError(); + + public Task InvokeAsync(Func call) => + Task.WhenAll(_callbacks.ToArray().Select(wrapper => wrapper.InvokeAsync(async callback => + { + try + { + await call(callback); + } + catch (Exception ex) when (ex is ObjectDisposedException || ex is IOException) + { + Trace.TraceInformation($"{nameof(Callbacks)}: Client {callback.GetHashCode()} exited: {ex.GetType()}"); + _callbacks.Remove(wrapper); + } + }))); +} + +internal sealed class Callback where T : class +{ + private readonly T _callback; + private readonly AsyncLock _lock = new AsyncLock(); + + public IClient Client { get; } + + public Callback(Message message) + { + Client = message.Client; + _callback = message.Client.GetCallback(); + } + + public void Invoke(Func call) => InvokeAsync(call).TraceError(); + + public async Task InvokeAsync(Func call) + { + using (await _lock.LockAsync()) + { + await call(_callback); + } + } + + public async Task FlushAsync() => (await _lock.LockAsync()).Dispose(); +} + +internal static class TaskExtensions +{ + public static void TraceError(this Task task, [CallerFilePath] string file = null!, [CallerMemberName] string member = null!, [CallerLineNumber] int line = default, string customMessage = default!) => + + task.ContinueWith(result => result.Exception?.Trace($"{nameof(TraceError)}: {file}:{member}:{line} {customMessage}\n"), TaskContinuationOptions.NotOnRanToCompletion); + public static string Trace(this Exception exception, string? label = null) + { + var content = exception.CreateTraceMessage(label); + System.Diagnostics.Trace.TraceError(content); + return content; + } + public static string CreateTraceMessage(this Exception exception, string? label = null) + { + var prefix = string.IsNullOrWhiteSpace(label) ? string.Empty : $"{label}: "; + return $"{prefix}{ExceptionToString()}, HResult {exception.HResult}"; + + string ExceptionToString() + { + try + { + return exception.ToString(); + } + catch (Exception toStringException) + { + return $"{exception.GetType()}: {exception.Message} ---> ToString() of this exception failed:{Environment.NewLine}{toStringException}"; + } + } + } + +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Services/SystemService.cs b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs new file mode 100644 index 00000000..0c96445f --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs @@ -0,0 +1,105 @@ +using Castle.Core; +using System.Buffers; +using System.Globalization; +using System.Text; + +namespace UiPath.Ipc.Tests; + +public sealed class SystemService : ISystemService +{ + public async Task EchoGuidAfter(Guid value, TimeSpan waitOnServer, Message? message = null, CancellationToken ct = default) + { + await Task.Delay(waitOnServer, ct); + return value; + } + + public async Task MessageReceivedAsNotNull(Message? message = null) + => message is not null; + + private volatile TaskCompletionSource? _tripWire = null; + internal Task ResetTripWire() => (_tripWire = new()).Task; + public const int MsFireAndForgetDelay = +#if CI + 400; +#else + 40; +#endif + public async Task FireAndForget(TimeSpan wait) + { + await Task.Delay(wait); + _tripWire?.TrySetResult(null); + } + + public Task EchoString(string value) => Task.FromResult(value); + + public async Task<(string ExceptionType, string ExceptionMessage, string? MarshalledExceptionType)?> CallUnregisteredCallback(Message message = null!) + { + try + { + _ = await message.Client.GetCallback().SomeMethod(); + return null; + } + catch (Exception ex) + { + return (ex.GetType().Name, ex.Message, (ex as RemoteException)?.Type); + } + } + + public Task FireAndForgetThrowSync() => throw new MarkerException(); + + public sealed class MarkerException : Exception { } + + public async Task GetThreadName() => Thread.CurrentThread.Name; + + public async Task UploadEcho(Stream stream, CancellationToken ct = default) + { + var bytes = await stream.ReadToEndAsync(ct); + return Encoding.UTF8.GetString(bytes); + } + + public async Task UploadJustCountBytes(Stream stream, int serverReadByteCount, TimeSpan serverDelay, CancellationToken ct = default) + { + var buffer = ArrayPool.Shared.Rent(serverReadByteCount); + try + { + await Task.Delay(serverDelay, ct); + await stream.ReadExactlyAsync(buffer, 0, serverReadByteCount, ct); + return true; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public async Task Download(string s, CancellationToken ct = default) + => new MemoryStream(Encoding.UTF8.GetBytes(s)); + + public async Task AddIncrement(int x, int y, Message message = null!) + { + var sum = await message.Client.GetCallback().AddInts(x, y); + var result = await message.Client.GetCallback().Increment(sum); + return result; + } + + public async Task FireAndForgetWithCt(CancellationToken ct) + { + await Task.Delay(100, ct); + } + + private static readonly Lazy Danish = new(() => new("da-DK")); + + public Task DanishNameOfDay(DayOfWeek day, CancellationToken ct) + => Task.FromResult(Danish.Value.DateTimeFormat.GetDayName(day)); + + public Task ReverseBytes(byte[] bytes, CancellationToken ct = default) + { + for (int i = 0, j = bytes.Length - 1; i < j; i++, j--) + { + var t = bytes[i]; + bytes[i] = bytes[j]; + bytes[j] = t; + } + return Task.FromResult(bytes); + } +} diff --git a/src/UiPath.CoreIpc.Tests/SpyTestBase.cs b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs new file mode 100644 index 00000000..2fc866f3 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs @@ -0,0 +1,20 @@ +using System.Collections.Concurrent; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public abstract class SpyTestBase : TestBase +{ + protected readonly ConcurrentBag _clientBeforeCalls = new(); + + protected SpyTestBase(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + protected override void ConfigureClient(IpcClient ipcClient) + { + base.ConfigureClient(ipcClient); + + ipcClient.BeforeOutgoingCall = async (callInfo, _) => _clientBeforeCalls.Add(callInfo); + } +} diff --git a/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs new file mode 100644 index 00000000..6992eeeb --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs @@ -0,0 +1,141 @@ +using UiPath.Ipc.Transport.NamedPipe; + +namespace UiPath.Ipc.Tests; + +public class SyncOverAsyncTests +{ + [Theory] + [InlineData(ScenarioId.Inline)] + [InlineData(ScenarioId.GuiLikeSynchronizationContext)] + [InlineData(ScenarioId.GuiLikeTaskScheduler)] + [InlineData(ScenarioId.ThreadPoolTaskScheduler +)] + public async Task RemoteCallingSyncOverAsync_IpcShouldBeResilient(ScenarioId scenarioId) + { + var pipeName = $"{Guid.NewGuid():N}"; + + await using var ipcServer = CreateServer(pipeName); + await ipcServer.WaitForStart(); + + var ipcClient = CreateClient(pipeName); + + var proxy = ipcClient.GetProxy(); + + var tcsDone = new TaskCompletionSource(); + + var scenario = scenarioId.CreateScenario(); + scenario.Run(() => + { + try + { + var result = proxy.AddFloats(2, 3).Result;//.GetAwaiter().GetResult(); + tcsDone.SetResult(result); + } + catch (OperationCanceledException) + { + tcsDone.SetCanceled(); + } + catch (Exception ex) + { + tcsDone.SetException(ex); + } + }); + + await tcsDone.Task.ShouldBeAsync(5).ShouldCompleteInAsync(TimeSpan.FromSeconds(20)); + } + + private static IpcServer CreateServer(string pipeName) + => new IpcServer + { + Transport = new NamedPipeServerTransport { PipeName = pipeName }, + Endpoints = new() + { + typeof(IComputingService) + }, + ServiceProvider = new ServiceCollection() + .AddLogging() + .AddSingleton() + .BuildServiceProvider() + }; + + private static IpcClient CreateClient(string pipeName) + => new() + { + Transport = new NamedPipeClientTransport { PipeName = pipeName } + }; + + + + public enum ScenarioId + { + Inline, + GuiLikeSynchronizationContext, + GuiLikeTaskScheduler, + ThreadPoolTaskScheduler + } + + public abstract class Scenario + { + public abstract void Run(Action action); + + public sealed class Inline : Scenario + { + public override void Run(Action action) => action(); + } + + public abstract class SynchronizationContextScenario : Scenario + { + private readonly Lazy _synchronizationContext; + + public SynchronizationContextScenario() => _synchronizationContext = new(CreateSynchronizationContext); + + protected abstract SynchronizationContext CreateSynchronizationContext(); + + public override void Run(Action action) + { + _synchronizationContext.Value.Post(_ => action(), state: null); + } + } + + public sealed class GuiLikeSynchronizationContext : SynchronizationContextScenario + { + protected override SynchronizationContext CreateSynchronizationContext() + => new Nito.AsyncEx.AsyncContextThread().Context.SynchronizationContext; + } + + public abstract class TaskSchedulerScenario : Scenario + { + private readonly Lazy _taskScheduler; + + public TaskSchedulerScenario() => _taskScheduler = new(CreateTaskScheduler); + + public override void Run(Action action) + { + _ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _taskScheduler.Value); + } + + protected abstract TaskScheduler CreateTaskScheduler(); + } + + public sealed class GuiLikeTaskScheduler : TaskSchedulerScenario + { + protected override TaskScheduler CreateTaskScheduler() => new ConcurrentExclusiveSchedulerPair().ConcurrentScheduler; + } + public sealed class ThreadPoolTaskScheduler : TaskSchedulerScenario + { + protected override TaskScheduler CreateTaskScheduler() => TaskScheduler.Default; + } + } +} + +internal static class SyncOverAsyncTests_ScenarioIdExtensions +{ + public static SyncOverAsyncTests.Scenario CreateScenario(this SyncOverAsyncTests.ScenarioId id) => id switch + { + SyncOverAsyncTests.ScenarioId.Inline => new SyncOverAsyncTests.Scenario.Inline(), + SyncOverAsyncTests.ScenarioId.GuiLikeSynchronizationContext => new SyncOverAsyncTests.Scenario.GuiLikeSynchronizationContext(), + SyncOverAsyncTests.ScenarioId.GuiLikeTaskScheduler => new SyncOverAsyncTests.Scenario.GuiLikeTaskScheduler(), + SyncOverAsyncTests.ScenarioId.ThreadPoolTaskScheduler => new SyncOverAsyncTests.Scenario.ThreadPoolTaskScheduler(), + _ => throw new ArgumentOutOfRangeException(nameof(id)), + }; +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/SystemTests.cs b/src/UiPath.CoreIpc.Tests/SystemTests.cs index e33832f4..70db74df 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTests.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTests.cs @@ -1,295 +1,421 @@ -using System.Text; +using AutoFixture; +using AutoFixture.Xunit2; +using Microsoft.Extensions.Hosting; +using System.Text; +using System.Threading.Channels; +using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -public abstract class SystemTests : TestBase where TBuilder : ServiceClientBuilder +public abstract class SystemTests : TestBase { - protected ServiceHost _systemHost; - protected ISystemService _systemClient; - protected readonly SystemService _systemService; - public SystemTests() - { - _systemService = (SystemService)_serviceProvider.GetService(); - _systemHost = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint() - .ValidateAndBuild(); - _systemHost.RunAsync(GuiScheduler); - _systemClient = CreateSystemService(); - } - protected override TSettings Configure(TSettings listenerSettings) + #region " Setup " + private readonly Lazy _service; + private readonly Lazy _proxy; + + protected SystemService Service => _service.Value; + protected ISystemService Proxy => _proxy.Value!; + + protected sealed override IpcProxy IpcProxy => Proxy as IpcProxy ?? throw new InvalidOperationException($"Proxy was expected to be a {nameof(IpcProxy)} but was not."); + protected sealed override Type ContractType => typeof(ISystemService); + + protected SystemTests(ITestOutputHelper outputHelper) : base(outputHelper) { - base.Configure(listenerSettings); - listenerSettings.ConcurrentAccepts = 10; - listenerSettings.RequestTimeout = RequestTimeout.Subtract(TimeSpan.FromSeconds(1)); - return listenerSettings; + ServiceProvider.InjectLazy(out _service); + CreateLazyProxy(out _proxy); } - public override void Dispose() + + protected override void ConfigureSpecificServices(IServiceCollection services) + => services + .AddSingleton() + .AddSingletonAlias(); + + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; + #endregion + + [Theory, IpcAutoData] + public async Task PassingArgsAndReturning_ShouldWork(Guid guid) { - ((IDisposable)_systemClient).Dispose(); - ((IpcProxy)_systemClient).CloseConnection(); - _systemHost.Dispose(); - base.Dispose(); + var clone = await Proxy.EchoGuidAfter(guid, TimeSpan.Zero); + clone.ShouldBe(guid); } - [Fact] - public async Task ConcurrentRequests() + + [Theory, IpcAutoData] + public async Task ConcurrentOperations_ShouldWork(Guid guid1, Guid guid2) { - var infinite = _systemClient.Infinite(); - await Guid(); - infinite.IsCompleted.ShouldBeFalse(); + using var cts = new CancellationTokenSource(); + var task1 = Proxy.EchoGuidAfter(guid1, Timeout.InfiniteTimeSpan, message: null, cts.Token); + + (await Proxy.EchoGuidAfter(guid2, TimeSpan.Zero)).ShouldBe(guid2); + + task1.IsCompleted.ShouldBeFalse(); + cts.Cancel(); + var act = () => task1.ShouldCompleteInAsync(Timeouts.LocalProxyToThrowOCE); + await act.ShouldThrowAsync(); } + [Fact] - public async Task OptionalMessage() - { - var returnValue = await _systemClient.ImpersonateCaller(); - returnValue.ShouldBe(Environment.UserName); - } + public async Task NotPassingAnOptionalMessage_ShouldWork() + => await Proxy + .MessageReceivedAsNotNull(message: null) + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip) + .ShouldNotThrowAsyncAnd() + .ShouldBeAsync(true); [Fact] - public async Task ServerTimeout() - { - var ex = _systemClient.Infinite().ShouldThrow(); - ex.Message.ShouldBe($"{nameof(_systemClient.Infinite)} timed out."); - ex.Is().ShouldBeTrue(); - await Guid(); - } + [OverrideConfig(typeof(ShortServerLongClientTimeout))] + public async Task ServerExecutingTooLongACall_ShouldThrowTimeout() + => await Proxy.EchoGuidAfter(Guid.Empty, Timeout.InfiniteTimeSpan) // method takes forever but we have a server side RequestTimeout configured + .ShouldThrowAsync() + .ShouldSatisfyAllConditionsAsync( + [ + ex => ex.Message.ShouldBe(TimeoutHelper.ComputeTimeoutMessage(nameof(Proxy.EchoGuidAfter))), + ex => ex.Is().ShouldBeTrue() + ]); + [Fact] - public async Task Void() + [OverrideConfig(typeof(ClientWaitingForTooLongACall_ShouldThrowTimeout_Config))] + public async Task ClientWaitingForTooLongACall_ShouldThrowTimeout() + => await Proxy.EchoGuidAfter(Guid.Empty, Timeout.InfiniteTimeSpan) // method takes forever but we have a server side RequestTimeout configured + .ShouldThrowAsync(); + + private sealed class ShortServerLongClientTimeout : OverrideConfig { - _systemService.DidNothing = false; - await _systemClient.DoNothing(); - _systemService.DidNothing.ShouldBeFalse(); - while (!_systemService.DidNothing) + public override async Task Override(Func> ipcServerFactory) { - await Task.Delay(10); - Trace.WriteLine(this + " Void"); + var ipcServer = await ipcServerFactory(); + ipcServer.RequestTimeout = Timeouts.Short; + return ipcServer; } + + public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeout.InfiniteTimeSpan); } - [Fact] - public async Task VoidThreadName() + private sealed class ClientWaitingForTooLongACall_ShouldThrowTimeout_Config : OverrideConfig { - await _systemClient.VoidThreadName(); - await _systemClient.GetThreadName(); - while (_systemService.ThreadName != "GuiThread") - { - await Task.Delay(0); - Trace.WriteLine(this + " VoidThreadName"); - } + public override Task Override(Func> ipcServerFactory) => ipcServerFactory().WithRequestTimeout(Timeout.InfiniteTimeSpan)!; + public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeouts.IpcRoundtrip); } [Fact] - public async Task Enum() + public async Task FireAndForget_ShouldWork() { - var text = await _systemClient.ConvertText("hEllO woRd!", TextStyle.Upper); - text.ShouldBe("HELLO WORD!"); + var taskRequestHonoured = Service.ResetTripWire(); + var wait = TimeSpan.FromSeconds(1); + + await Proxy.FireAndForget(wait).ShouldCompleteInAsync(Timeouts.IpcRoundtrip + Timeouts.IpcRoundtrip); + taskRequestHonoured.IsCompleted.ShouldBeFalse(); + + await taskRequestHonoured.ShouldCompleteInAsync(Timeouts.IpcRoundtrip + wait + wait); } [Fact] - public async Task PropertyWithTypeDefaultValue() + public async Task ExceedingMsgSize_ShouldBreakNetwork_ButShouldBeRecoverable() { - var args = new ConvertTextArgs { Text = "hEllO woRd!", TextStyle = default }; - var text = await _systemClient.ConvertTextWithArgs(args); - text.ShouldBe("Hello Word!"); + const string Little = "a"; + + const int KB = 1024; + const int MB = 1024 * KB; + var TooBig = new string('a', 2 * MB); + + // Prime the connection + await Proxy.EchoString(Little).ShouldBeAsync(Little); + var originalNetwork = (Proxy as IpcProxy)!.Network! + .ShouldNotBeNull(); + + // Send a message that is too big, the network should be closed + await Proxy.EchoString(TooBig).ShouldThrowAsync(); + + // Send a regular message, the connection should be reestablished + await Proxy.EchoString(Little).ShouldBeAsync(Little); + + (Proxy as IpcProxy)!.Network! + .ShouldNotBeNull() + .ShouldNotBeSameAs(originalNetwork); } [Fact] - public async Task MaxMessageSize() + public async Task ServerCallingInexistentCallback_ShouldThrow() { - _systemClient.ReverseBytes(new byte[MaxReceivedMessageSizeInMegabytes * 1024 * 1024]).ShouldThrow(); - await Guid(); + var (exceptionType, exceptionMessage, marshalledExceptionType) = (await Proxy.CallUnregisteredCallback()).ShouldNotBeNull(); + exceptionType.ShouldBe(nameof(RemoteException)); + marshalledExceptionType.ShouldBe(typeof(EndpointNotFoundException).FullName); } [Fact] - public async Task Guid() + public async Task ServerCallingInexistentCallback_ShouldThrow2() + => await Proxy.AddIncrement(1, 2).ShouldThrowAsync() + .ShouldSatisfyAllConditionsAsync([ + ex => ex.Is() + ]); + + [Fact, OverrideConfig(typeof(RegisterCallbacks))] + public async Task ServerCallingMultipleCallbackTypes_ShouldWork() + => await Proxy.AddIncrement(1, 2).ShouldBeAsync(1 + 2 + 1); + + private sealed class RegisterCallbacks : OverrideConfig { - var newGuid = System.Guid.NewGuid(); - var guid = await _systemClient.GetGuid(newGuid); - guid.ShouldBe(newGuid); + public override IpcClient? Override(Func client) + => client().WithCallbacks(new() + { + { typeof(IComputingCallback), new ComputingCallback() }, + { typeof(IArithmeticCallback), new ArithmeticCallback() }, + }); } [Fact] - public Task LargeMessage() => _systemClient.ReverseBytes(new byte[(int)(0.7 * MaxReceivedMessageSizeInMegabytes * 1024 * 1024)]); + public async Task FireAndForgetOperations_ShouldNotDeliverBusinessExceptionsEvenWhenThrownSynchronously() + => await Proxy.FireAndForgetThrowSync() + .ShouldNotThrowAsync() + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip); [Fact] - public async Task ReverseBytes() + public async Task ServerScheduler_ShouldBeUsed() + => await Proxy.GetThreadName() + .ShouldBeAsync(Names.GuiThreadName); + + [Theory, IpcAutoData] + public async Task UploadingStreams_ShouldWork(string str) { - var input = Encoding.UTF8.GetBytes("Test"); - var reversed = await _systemClient.ReverseBytes(input); - reversed.ShouldBe(input.Reverse()); + using var memory = new MemoryStream(Encoding.UTF8.GetBytes(str)); + await Proxy.UploadEcho(memory).ShouldBeAsync(str); } - [Fact] - public async Task MissingCallback() + //[Theory, IpcAutoData] + public async Task CancelingStreamUploads_ShouldThrow(string str, Guid guid) { - RemoteException exception = null; - try - { - await _systemClient.MissingCallback(new SystemMessage()); - } - catch (RemoteException ex) - { - exception = ex; - } - exception.Message.ShouldBe("Callback contract mismatch. Requested System.IDisposable, but it's not configured."); - exception.Is().ShouldBeTrue(); - await Guid(); - } + var sourceMemory = new Memory(Encoding.UTF8.GetBytes(str)); + using var cts = new CancellationTokenSource(); + using var stream = new UploadStream(); - [Fact] - public async Task VoidIsAsync() => await _systemClient.VoidSyncThrow(); + var taskReadCall = stream.AwaitReadCall(); - [Fact] - public async Task GetThreadName() => (await _systemClient.GetThreadName()).ShouldBe("GuiThread"); + var taskUploading = Proxy.UploadEcho(stream, cts.Token); - [Fact] - public async Task Echo() + var readCall = await taskReadCall.ShouldCompleteInAsync(TimeSpan.FromSeconds(60));// Constants.Timeout_IpcRoundtrip); + stream.AutoRespondByte = (byte)'a'; + var cbRead = Math.Min(readCall.Memory.Length, sourceMemory.Length); + var sourceSlice = sourceMemory.Slice(start: 0, cbRead); + sourceSlice.CopyTo(readCall.Memory); + var expectedServerRead = Encoding.UTF8.GetString(sourceSlice.ToArray()); + + readCall.Return(cbRead); + + taskUploading.IsCompleted.ShouldBeFalse(); + + await Task.Delay(Timeouts.IpcRoundtrip); // we just replied to the read call, but canceling during stream uploads works by destroying the network + var networkBeforeCancel = IpcProxy.Network; + cts.Cancel(); + + await taskUploading + .ShouldThrowAsync() + .ShouldCompleteInAsync(Timeouts.Short); // in-process scheduling fast + + await Proxy.EchoGuidAfter(guid, waitOnServer: TimeSpan.Zero) // we expect the connection to recover + .ShouldBeAsync(guid); + + IpcProxy.Network.ShouldNotBeNull().ShouldNotBeSameAs(networkBeforeCancel); // and the network to be a new one + } + + [Theory, IpcAutoData] + public async Task UnfinishedUploads_ShouldThrowOnTheClient_AndRecover(Guid guid) { - using var stream = await _systemClient.Echo(new MemoryStream(Encoding.UTF8.GetBytes("Hello world"))); - (await new StreamReader(stream).ReadToEndAsync()).ShouldBe("Hello world"); + var stream = new UploadStream() { AutoRespondByte = 0 }; + + await Proxy.UploadJustCountBytes(stream, serverReadByteCount: 1, TimeSpan.Zero) // the server method deliberately returns before finishing to read the entire stream + .ShouldThrowAsync(); + + var act = async () => + { + while (true) + { + try + { + var actual = await Proxy.EchoGuidAfter(guid, TimeSpan.Zero); + actual.ShouldBe(guid); + return; + } + catch + { + } + await Task.Delay(100); + } + }; + await act().ShouldCompleteInAsync(TimeSpan.FromSeconds(5)); } - [Fact] - public async Task CancelUpload() +#if !CI + [Theory, IpcAutoData] +#endif + public async Task UnfinishedUploads_ShouldThrowOnTheClient_AndRecover_Repeat(Guid guid) { - var stream = new MemoryStream(Enumerable.Range(1, 50000).Select(i=>(byte)i).ToArray()); - await _systemClient.GetThreadName(); - using (var cancellationSource = new CancellationTokenSource(5)) + const int IterationCount = 500; + foreach (var i in Enumerable.Range(1, IterationCount)) { - _systemClient.Upload(stream, 20, cancellationSource.Token).ShouldThrow(); + _outputHelper.WriteLine($"Starting iteration {i}/{IterationCount}..."); + await UnfinishedUploads_ShouldThrowOnTheClient_AndRecover(guid); + _outputHelper.WriteLine($"Finished iteration {i}/{IterationCount}."); } } - [Fact] - public async Task Upload() + [Theory, IpcAutoData] + public async Task DownloadingStreams_ShouldWork(string str) { - (await _systemClient.Upload(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBe("Hello world"); - await Guid(); + using var stream = await Proxy.Download(str); + using var reader = new StreamReader(stream); + var clone = await reader.ReadToEndAsync(); + clone.ShouldBe(str); } - [Fact] - public virtual async Task UploadNoRead() + public static IEnumerable DownloadingStreams_ShouldWork_Repeat_Cases() { - try + var fixture = IpcAutoDataAttribute.CreateFixture(); + const int CTimes = 100; + + foreach (var time in Enumerable.Range(1, CTimes)) { - (await _systemClient.UploadNoRead(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBeEmpty(); + yield return [time, fixture.Create()]; } - catch (IOException) { } - catch (ObjectDisposedException) { } - await Guid(); } - [Fact] - public Task DownloadUiThread() => Task.Factory.StartNew(Download, default, TaskCreationOptions.DenyChildAttach, GuiScheduler).Unwrap(); - [Fact] - public async Task Download() + [Theory, IpcAutoData] + public async Task StreamDownloadsClosedUnfinished_ShouldNotAffectTheConnection(string str, Guid guid) { - using var stream = await _systemClient.Download("Hello world"); - (await new StreamReader(stream).ReadToEndAsync()).ShouldBe("Hello world"); - } - [Fact] - public async Task DownloadNoRead() - { - using (await _systemClient.Download("Hello world")) { } - await Guid(); + using (var stream = await Proxy.Download(str)) + { + } + + await Proxy.EchoGuidAfter(guid, TimeSpan.Zero) + .ShouldBeAsync(guid) + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip); } - protected abstract TBuilder CreateSystemClientBuilder(); - protected TBuilder SystemClientBuilder() => CreateSystemClientBuilder().SerializeParametersAsObjects().RequestTimeout(RequestTimeout).Logger(_serviceProvider); - [Fact] - public async Task BeforeCall() + + [Theory, IpcAutoData] + public async Task StreamDownloadsLeftOpen_WillHijackTheConnection(string str, Guid guid) { - bool newConnection = false; - var proxy = SystemClientBuilder().BeforeCall(async (c, _) => + using (var stream = await Proxy.Download(str)) { - newConnection = c.NewConnection; - c.Method.ShouldBe(typeof(ISystemService).GetMethod(nameof(ISystemService.DoNothing))); - c.Arguments.Single().ShouldBe(""); // cancellation token - }).ValidateAndBuild(); - newConnection.ShouldBeFalse(); - - await proxy.DoNothing(); - newConnection.ShouldBeTrue(); - - await proxy.DoNothing(); - newConnection.ShouldBeFalse(); - var ipcProxy = (IpcProxy)proxy; - var closed = false; - ipcProxy.Connection.Closed += delegate { closed = true; }; - ipcProxy.CloseConnection(); - closed.ShouldBeTrue(); - newConnection.ShouldBeFalse(); - await proxy.DoNothing(); - newConnection.ShouldBeTrue(); - - await proxy.DoNothing(); - newConnection.ShouldBeFalse(); - ipcProxy.CloseConnection(); + await new StreamReader(stream).ReadToEndAsync() + .ShouldBeAsync(str); + + await Proxy.EchoGuidAfter(guid, waitOnServer: TimeSpan.Zero, message: new() { RequestTimeout = Timeout.InfiniteTimeSpan }) + .ShouldStallForAtLeastAsync(Timeouts.IpcRoundtrip); + } } - [Fact] - public async Task DontReconnect() +#if !CI + [Theory, IpcAutoData] +#endif + public async Task StreamDownloadsLeftOpen_WillHijackTheConnection_Repeat(string str, Guid guid) { - var proxy = SystemClientBuilder().DontReconnect().ValidateAndBuild(); - await proxy.GetGuid(System.Guid.Empty); - ((IpcProxy)proxy).CloseConnection(); - ObjectDisposedException exception = null; - try + const int IterationCount = 20; + foreach (var i in Enumerable.Range(0, IterationCount)) { - await proxy.GetGuid(System.Guid.Empty); + await StreamDownloadsLeftOpen_WillHijackTheConnection(str, guid); } - catch (ObjectDisposedException ex) + } + + [Theory, IpcAutoData] + public async Task IpcServerDispose_ShouldBeIdempotent(Guid guid) + { + await Proxy.EchoGuidAfter(guid, waitOnServer: default).ShouldBeAsync(guid); + var infiniteTask = Proxy.EchoGuidAfter(guid, Timeout.InfiniteTimeSpan); + + using (var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => services.AddHostedSingleton()) + .Build()) { - exception = ex; + await host.StartAsync(); + var hostedIpcServer = host.Services.GetRequiredService(); + hostedIpcServer.Set(IpcServer!); + await host.StopAsync(); } - exception.ShouldNotBeNull(); + + await IpcServer!.DisposeAsync(); + await IpcServer!.DisposeAsync(); + await infiniteTask.ShouldThrowAsync().ShouldCompleteInAsync(Timeouts.IpcRoundtrip); } - [Fact] - public Task CancelServerCall() => CancelServerCallCore(10); - protected ISystemService CreateSystemService() => SystemClientBuilder().ValidateAndBuild(); - async Task CancelServerCallCore(int counter) + private sealed class UploadStream : StreamBase { - for (int i = 0; i < counter; i++) + private readonly Channel _readCalls = Channel.CreateUnbounded(); + + public byte? AutoRespondByte { get; set; } + + public async Task AwaitReadCall(CancellationToken ct = default) => await _readCalls.Reader.ReadAsync(ct); + + public override long Length => long.MaxValue; + public override bool CanRead => true; + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var request = new SystemMessage { RequestTimeout = Timeout.InfiniteTimeSpan, Delay = Timeout.Infinite }; - var sendMessageResult = _systemClient.MissingCallback(request); - var newGuid = System.Guid.NewGuid(); - (await _systemClient.GetGuid(newGuid)).ShouldBe(newGuid); - await Task.Delay(1); - ((IpcProxy)_systemClient).CloseConnection(); - sendMessageResult.ShouldThrow(); - newGuid = System.Guid.NewGuid(); - (await _systemClient.GetGuid(newGuid)).ShouldBe(newGuid); + if (AutoRespondByte is { } @byte) + { + if (@byte > 0) + { + buffer.AsSpan().Slice(offset, count).Fill(@byte); + } + + return Task.FromResult(count); + } + + var memory = new Memory(buffer, offset, count); + var call = new ReadCall(out var task) + { + Memory = new(buffer, offset, count), + CancellationToken = cancellationToken + }; + + if (!_readCalls.Writer.TryWrite(call)) + { + throw new InvalidOperationException(); + } + + return task; + } + + public sealed class ReadCall + { + public required Memory Memory { get; init; } + public required CancellationToken CancellationToken { get; init; } + + private readonly TaskCompletionSource _tcs = new(); + + public ReadCall(out Task task) => task = _tcs.Task; + + public void Return(int cbRead) => _tcs.TrySetResult(cbRead); } } - [Fact] - public async Task ClosingTheHostShouldCloseTheConnection() + + private interface IHostedIpcServer { - var request = new SystemMessage { RequestTimeout = Timeout.InfiniteTimeSpan, Delay = Timeout.Infinite }; - var sendMessageResult = _systemClient.MissingCallback(request); - var newGuid = System.Guid.NewGuid(); - (await _systemClient.GetGuid(newGuid)).ShouldBe(newGuid); - await Task.Delay(1); - _systemHost.Dispose(); - sendMessageResult.ShouldThrow(); + void Set(IpcServer ipcServer); } - [Fact] - public virtual async void BeforeCallServerSide() + + private sealed class HostedIpcServer : IHostedService, IHostedIpcServer, IAsyncDisposable { - var newGuid = System.Guid.NewGuid(); - MethodInfo method = null; - using var protectedService = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint(new EndpointSettings + private IpcServer? _ipcServer; + + public void Set(IpcServer ipcServer) => _ipcServer = ipcServer; + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _ipcServer!.DisposeAsync(); + } + + public async ValueTask DisposeAsync() + { + try { - BeforeCall = async (call, ct) => - { - method = call.Method; - call.Arguments[0].ShouldBe(newGuid); - } - }) - .ValidateAndBuild(); - _ = protectedService.RunAsync(); - await CreateSystemService().GetGuid(newGuid); - method.ShouldBe(typeof(ISystemService).GetMethod(nameof(ISystemService.GetGuid))); + await _ipcServer!.DisposeAsync(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + throw; + } + } } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs new file mode 100644 index 00000000..27f83f70 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs @@ -0,0 +1,21 @@ +using UiPath.Ipc.Transport.NamedPipe; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public sealed class SystemTestsOverNamedPipes : SystemTests +{ + private string PipeName => Names.GetPipeName(role: "system", TestRunId); + + public SystemTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected sealed override async Task CreateServerTransport() => new NamedPipeServerTransport + { + PipeName = PipeName + }; + protected sealed override ClientTransport CreateClientTransport() => new NamedPipeClientTransport() + { + PipeName = PipeName, + AllowImpersonation = true, + }; +} diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs new file mode 100644 index 00000000..d9878b51 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs @@ -0,0 +1,21 @@ +using System.Net; +using UiPath.Ipc.Transport.Tcp; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public sealed class SystemTestsOverTcp : SystemTests +{ + private readonly IPEndPoint _endPoint = NetworkHelper.FindFreeLocalPort(); + + public SystemTestsOverTcp(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected sealed override async Task CreateServerTransport() + => new TcpServerTransport + { + EndPoint = _endPoint, + }; + + protected override ClientTransport CreateClientTransport() + => new TcpClientTransport() { EndPoint = _endPoint }; +} diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs new file mode 100644 index 00000000..bc17f6f6 --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs @@ -0,0 +1,31 @@ +using UiPath.Ipc.Transport.WebSocket; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public sealed class SystemTestsOverWebSockets : SystemTests +{ + private readonly WebSocketContext _webSocketContext = new(); + + public SystemTestsOverWebSockets(ITestOutputHelper outputHelper) : base(outputHelper) { } + + protected override async Task DisposeAsync() + { + await _webSocketContext.DisposeAsync(); + await base.DisposeAsync(); + } + + protected override async Task CreateServerTransport() + { + var listener = new WebSocketServerTransport + { + Accept = _webSocketContext.Accept, + ConcurrentAccepts = 1, + }; + await Task.Delay(500); // Wait for the listener to start. + return listener; + } + + protected override ClientTransport CreateClientTransport() + => new WebSocketClientTransport() { Uri = _webSocketContext.ClientUri }; +} diff --git a/src/UiPath.CoreIpc.Tests/TcpTests..cs b/src/UiPath.CoreIpc.Tests/TcpTests..cs deleted file mode 100644 index cca919c5..00000000 --- a/src/UiPath.CoreIpc.Tests/TcpTests..cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; -using UiPath.CoreIpc.Tcp; -namespace UiPath.CoreIpc.Tests; -public class SystemTcpTests : SystemTests> -{ - int _port = 3131 + GetCount(); - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => - serviceHostBuilder.UseTcp(Configure(new TcpSettings(GetEndPoint()))); - protected override TcpClientBuilder CreateSystemClientBuilder() => new(GetEndPoint()); - [Fact] - public override async void BeforeCallServerSide() - { - _port++; - base.BeforeCallServerSide(); - } - IPEndPoint GetEndPoint() => new(IPAddress.Loopback, _port); -} -public class ComputingTcpTests : ComputingTests> -{ - protected static readonly IPEndPoint ComputingEndPoint = new(IPAddress.Loopback, 2121+GetCount()); - protected override TcpClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => - new TcpClientBuilder(ComputingEndPoint, _serviceProvider) - .RequestTimeout(RequestTimeout) - .CallbackInstance(_computingCallback) - .TaskScheduler(taskScheduler); - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => - serviceHostBuilder.UseTcp(Configure(new TcpSettings(ComputingEndPoint))); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/TestBase.cs b/src/UiPath.CoreIpc.Tests/TestBase.cs index 98d75a4e..1318aeef 100644 --- a/src/UiPath.CoreIpc.Tests/TestBase.cs +++ b/src/UiPath.CoreIpc.Tests/TestBase.cs @@ -1,39 +1,166 @@ using Nito.AsyncEx; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -public abstract class TestBase : IDisposable +public abstract class TestBase : IAsyncLifetime { - protected const int MaxReceivedMessageSizeInMegabytes = 1; - protected static int Count = -1; - public static readonly TimeSpan RequestTimeout = -#if CI - TimeSpan.FromSeconds(2) + + protected readonly ITestOutputHelper _outputHelper; + private readonly IMethodInfo _xUnitMethod; + private readonly ServiceProvider _serviceProvider; + private readonly AsyncContext _guiThread = new AsyncContextThread().Context; + private readonly Lazy> _ipcServer; + private readonly Lazy _ipcClient; + private readonly OverrideConfig? _overrideConfig; + + protected TestRunId TestRunId { get; } = TestRunId.New(); + protected IServiceProvider ServiceProvider => _serviceProvider; + protected TaskScheduler GuiScheduler => _guiThread.Scheduler; + protected IpcServer? IpcServer { get; private set; } + protected abstract IpcProxy? IpcProxy { get; } + protected abstract Type ContractType { get; } + + protected readonly ConcurrentBag _serverBeforeCalls = new(); + protected BeforeCallHandler? _tailBeforeCall = null; + + public TestBase(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + + _xUnitMethod = CustomTestFramework.Context?.Method ?? throw new InvalidOperationException(); + + string runtime = +#if NET461 + RuntimeInformation.FrameworkDescription +#else + $"{RuntimeInformation.FrameworkDescription}, {RuntimeInformation.RuntimeIdentifier}" #endif - (Debugger.IsAttached ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(2)); - protected readonly IServiceProvider _serviceProvider; - protected readonly AsyncContext _guiThread = new AsyncContextThread().Context; - - //static TestBase() - //{ - // AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false); - //} - public TestBase() + ; + _outputHelper.WriteLine($"[{runtime}] \"{_xUnitMethod.Name}\""); + _outputHelper.WriteLine("--------------------------------------\r\n"); + _overrideConfig = GetOverrideConfig(); + + _guiThread.SynchronizationContext.Send(() => Thread.CurrentThread.Name = Names.GuiThreadName); + _serviceProvider = IpcHelpers.ConfigureServices(_outputHelper, ConfigureSpecificServices); + + _ipcServer = new(CreateIpcServer); + _ipcClient = new(() => CreateIpcClient()); + + OverrideConfig? GetOverrideConfig() + { + var xUnitMethod = _xUnitMethod ?? throw new InvalidOperationException(); + + var overrideConfigType = xUnitMethod + .GetCustomAttributes(typeof(OverrideConfigAttribute)) + .SingleOrDefault()?.GetConstructorArguments() + .SingleOrDefault() as Type; + + if (overrideConfigType is null) + { + return null; + } + return Activator.CreateInstance(overrideConfigType) as OverrideConfig; + } + } + + protected abstract void ConfigureSpecificServices(IServiceCollection services); + + protected virtual ContractCollection? Callbacks => []; + + private Task CreateIpcServer() { - _guiThread.SynchronizationContext.Send(() => Thread.CurrentThread.Name = "GuiThread"); - _serviceProvider = IpcHelpers.ConfigureServices(); + if (_overrideConfig is not null) + { + return _overrideConfig.Override(Core); + } + + return Core()!; + + async Task Core() + { + _outputHelper.WriteLine($"Creating {nameof(ServerTransport)}..."); + + var serverTransport = await CreateServerTransport(); + ConfigTransportBase(serverTransport); + + var endpointSettings = new ContractSettings(ContractType) + { + BeforeIncomingCall = (callInfo, ct) => + { + _serverBeforeCalls.Add(callInfo); + return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask; + } + }; + + return new() + { + Endpoints = new() { endpointSettings }, + Transport = serverTransport, + ServiceProvider = _serviceProvider, + Scheduler = GuiScheduler, + RequestTimeout = ServerRequestTimeout + }; + } } + protected IpcClient? CreateIpcClient(ContractCollection? callbacks = null) + { + if (_overrideConfig is null) + { + return CreateDefaultClient(); + } - protected static int GetCount() => Interlocked.Increment(ref Count); + return _overrideConfig.Override(CreateDefaultClient); - protected TaskScheduler GuiScheduler => _guiThread.Scheduler; + IpcClient CreateDefaultClient() + { + var client = new IpcClient + { + Callbacks = callbacks ?? Callbacks, + Transport = CreateClientTransport() + }; + ConfigureClient(client); + return client; + } + } + + protected TContract? GetProxy() where TContract : class + => _ipcClient.Value?.GetProxy(); + + protected void CreateLazyProxy(out Lazy lazy) where TContract : class => lazy = new(GetProxy); + + protected abstract Task CreateServerTransport(); + protected abstract TimeSpan ServerRequestTimeout { get; } + + protected virtual void ConfigureClient(IpcClient ipcClient) + { + ipcClient.RequestTimeout = Timeouts.DefaultRequest; + ipcClient.Scheduler = GuiScheduler; + } + protected abstract ClientTransport CreateClientTransport(); + + protected virtual void ConfigTransportBase(ServerTransport serverTransport) + { + serverTransport.ConcurrentAccepts = 10; + serverTransport.MaxReceivedMessageSizeInMegabytes = 1; + } - public virtual void Dispose() => _guiThread.Dispose(); - protected virtual TSettings Configure(TSettings listenerSettings) where TSettings : ListenerSettings + protected virtual async Task DisposeAsync() { - listenerSettings.RequestTimeout = RequestTimeout; - listenerSettings.MaxReceivedMessageSizeInMegabytes = MaxReceivedMessageSizeInMegabytes; - return listenerSettings; + IpcProxy?.Dispose(); + await (IpcProxy?.CloseConnection() ?? default); + await (IpcServer?.DisposeAsync() ?? default); + _guiThread.Dispose(); + await _serviceProvider.DisposeAsync(); } - protected abstract ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder); -} \ No newline at end of file + + async Task IAsyncLifetime.InitializeAsync() + { + IpcServer = await _ipcServer.Value; + IpcServer?.Start(); + await (IpcServer?.WaitForStart() ?? Task.CompletedTask); + } + + Task IAsyncLifetime.DisposeAsync() => DisposeAsync(); +} diff --git a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj index eaf03da6..53f000ee 100644 --- a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj +++ b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj @@ -1,30 +1,58 @@  - net6.0;net461;net6.0-windows + WinExe + net6.0;net461 + UiPath.Ipc.Tests + UiPath.Ipc.Tests $(NoWarn);1998 $(DefineConstants);$(DefineConstantsEx) latest true + enable + false + + + + + + + + + + + + + - - - + + + + + + + + + + - - + + + - + + + - - \ No newline at end of file + + diff --git a/src/UiPath.CoreIpc.Tests/ValidationTests.cs b/src/UiPath.CoreIpc.Tests/ValidationTests.cs deleted file mode 100644 index a1a0197c..00000000 --- a/src/UiPath.CoreIpc.Tests/ValidationTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace UiPath.CoreIpc.Tests; - -public class ValidationTests -{ - class JobFailedException : Exception - { - public JobFailedException(Error error) : base("Job has failed.", new RemoteException(error)) - { - } - } - - [Fact] - public void ErrorFromRemoteException() - { - var innerError = new InvalidDataException("invalid").ToError(); - var error = new JobFailedException(innerError).ToError(); - error.Type.ShouldBe(typeof(JobFailedException).FullName); - error.Message.ShouldBe("Job has failed."); - error.InnerError.Type.ShouldBe(typeof(InvalidDataException).FullName); - error.InnerError.Message.ShouldBe("invalid"); - } - [Fact] - public void SerializeDefaultValueToString() => new IpcJsonSerializer().Serialize(new Message(0)).ShouldBe("{\"Payload\":0}"); - [Fact] - public void SerializeNullToString() => new IpcJsonSerializer().Serialize(new Message(null)).ShouldBe("{\"Payload\":null}"); -#if DEBUG - [Fact] - public void MethodsMustReturnTask() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("Method does not return Task!"); - [Fact] - public void DuplicateMessageParameters() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("The message must be the last parameter before the cancellation token!"); - [Fact] - public void TheMessageMustBeTheLastBeforeTheToken() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("The message must be the last parameter before the cancellation token!"); - [Fact] - public void CancellationTokenMustBeLast() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("The CancellationToken parameter must be the last!"); - [Fact] - public void UploadMustReturn() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("Upload methods must return a value!"); - [Fact] - public void DuplicateStreams() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("Only one Stream parameter is allowed!"); - [Fact] - public void UploadDerivedStream() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("Stream parameters must be typed as Stream!"); - [Fact] - public void DownloadDerivedStream() => new Action(() => new NamedPipeClientBuilder("").ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("Stream parameters must be typed as Stream!"); - [Fact] - public void TheCallbackContractMustBeAnInterface() => new Action(() => new NamedPipeClientBuilder("", IpcHelpers.ConfigureServices()).ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("The contract must be an interface!"); - [Fact] - public void TheServiceContractMustBeAnInterface() => new Action(() => new ServiceHostBuilder(IpcHelpers.ConfigureServices()).AddEndpoint().ValidateAndBuild()).ShouldThrow().Message.ShouldStartWith("The contract must be an interface!"); -#endif -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/WebSocketTests.cs b/src/UiPath.CoreIpc.Tests/WebSocketTests.cs deleted file mode 100644 index 0661160c..00000000 --- a/src/UiPath.CoreIpc.Tests/WebSocketTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using UiPath.CoreIpc.WebSockets; -namespace UiPath.CoreIpc.Tests; -public class SystemWebSocketTests : SystemTests> -{ - int _port = 1313 + GetCount(); - HttpSysWebSocketsListener _listener; - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) - { - _listener = new HttpSysWebSocketsListener("http" + GetEndPoint()); - return serviceHostBuilder.UseWebSockets(Configure(new WebSocketSettings(_listener.Accept))); - } - public override void Dispose() - { - base.Dispose(); - _listener?.Dispose(); - } - protected override WebSocketClientBuilder CreateSystemClientBuilder() => new(new("ws"+GetEndPoint())); - [Fact] - public override async void BeforeCallServerSide() - { - _port++; - base.BeforeCallServerSide(); - } -#if !NET461 - [Fact(Skip = "WebSocket.State is unreliable")] - public override Task UploadNoRead() => base.UploadNoRead(); -#endif - string GetEndPoint() => $"://localhost:{_port}/"; -} -public class ComputingWebSocketsTests : ComputingTests> -{ - protected static readonly string ComputingEndPoint = $"://localhost:{1212+GetCount()}/"; - HttpSysWebSocketsListener _listener; - protected override WebSocketClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => - new WebSocketClientBuilder(new("ws"+ComputingEndPoint), _serviceProvider) - .RequestTimeout(RequestTimeout) - .CallbackInstance(_computingCallback) - .TaskScheduler(taskScheduler); - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) - { - _listener = new HttpSysWebSocketsListener("http" + ComputingEndPoint); - return serviceHostBuilder.UseWebSockets(Configure(new WebSocketSettings(_listener.Accept))); - } - public override void Dispose() - { - base.Dispose(); - _listener?.Dispose(); - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs new file mode 100644 index 00000000..b954340b --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs @@ -0,0 +1,120 @@ +using Xunit.Abstractions; +using Xunit.Sdk; + +[assembly: TestFramework(typeName: "UiPath.Ipc.Tests.CustomTestFramework", assemblyName: "UiPath.Ipc.Tests")] + +namespace UiPath.Ipc.Tests; + +public readonly struct CustomTestContext +{ + public required IMethodInfo Method { get; init; } +} + +public class CustomTestFramework : XunitTestFramework +{ + private static readonly AsyncLocal AsyncLocalContext = new(); + public static CustomTestContext? Context => AsyncLocalContext.Value; + + public CustomTestFramework(IMessageSink messageSink) + : base(messageSink) + { + messageSink.OnMessage(new DiagnosticMessage($"Using {nameof(CustomTestFramework)}")); + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + => new CustomExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + + private class CustomExecutor : XunitTestFrameworkExecutor + { + public CustomExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink) + : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) + { + using var assemblyRunner = new CustomAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions); + await assemblyRunner.RunAsync(); + } + } + + private class CustomAssemblyRunner : XunitTestAssemblyRunner + { + public CustomAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) + : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) + { + } + + protected override Task RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource) + => new CustomTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + } + + private class CustomTestCollectionRunner : XunitTestCollectionRunner + { + public CustomTestCollectionRunner(ITestCollection testCollection, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) + : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) + { + } + + protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases) + => new CustomTestClassRunner(testClass, @class, testCases, DiagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings).RunAsync(); + } + + private class CustomTestClassRunner : XunitTestClassRunner + { + public CustomTestClassRunner(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) + : base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings) + { + } + + protected override Task RunTestMethodAsync(ITestMethod testMethod, IReflectionMethodInfo method, IEnumerable testCases, object[] constructorArguments) + => new CustomTestMethodRunner(testMethod, Class, method, testCases, DiagnosticMessageSink, MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, constructorArguments).RunAsync(); + } + + private class CustomTestMethodRunner : XunitTestMethodRunner + { + private readonly IMessageSink _diagnosticMessageSink; + + public CustomTestMethodRunner(ITestMethod testMethod, IReflectionTypeInfo @class, IReflectionMethodInfo method, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, object[] constructorArguments) + : base(testMethod, @class, method, testCases, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource, constructorArguments) + { + _diagnosticMessageSink = diagnosticMessageSink; + } + + protected override async Task RunTestCaseAsync(IXunitTestCase testCase) + { + var parameters = string.Empty; + + if (testCase.TestMethodArguments != null) + { + parameters = string.Join(", ", testCase.TestMethodArguments.Select(a => a?.ToString() ?? "null")); + } + + var test = $"{TestMethod.TestClass.Class.Name}.{TestMethod.Method.Name}({parameters})"; + + _diagnosticMessageSink.OnMessage(new DiagnosticMessage($"STARTED: {test}")); + + try + { + AsyncLocalContext.Value = new() + { + Method = testCase.Method + }; + var result = await base.RunTestCaseAsync(testCase); + + var status = result.Failed > 0 + ? "FAILURE" + : (result.Skipped > 0 ? "SKIPPED" : "SUCCESS"); + + _diagnosticMessageSink.OnMessage(new DiagnosticMessage($"{status}: {test} ({result.Time}s)")); + + return result; + } + catch (Exception ex) + { + _diagnosticMessageSink.OnMessage(new DiagnosticMessage($"ERROR: {test} ({ex.Message})")); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Client/CallInfo.cs b/src/UiPath.CoreIpc/Client/CallInfo.cs new file mode 100644 index 00000000..1d30a148 --- /dev/null +++ b/src/UiPath.CoreIpc/Client/CallInfo.cs @@ -0,0 +1,14 @@ +namespace UiPath.Ipc; + +public readonly struct CallInfo +{ + public CallInfo(bool newConnection, MethodInfo method, object?[] arguments) + { + NewConnection = newConnection; + Method = method; + Arguments = arguments; + } + public bool NewConnection { get; } + public MethodInfo Method { get; } + public object?[] Arguments { get; } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs b/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs deleted file mode 100644 index 14b708fc..00000000 --- a/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace UiPath.CoreIpc; - -static class ClientConnectionsRegistry -{ - private static readonly ConcurrentDictionary Connections = new(); - public static async Task GetOrCreate(IConnectionKey key, CancellationToken cancellationToken) - { - var clientConnection = GetOrAdd(key); - await clientConnection.Lock(cancellationToken); - try - { - // check again just in case it was removed after GetOrAdd but before entering the lock - ClientConnection newClientConnection; - while ((newClientConnection = GetOrAdd(key)) != clientConnection) - { - clientConnection.Release(); - await newClientConnection.Lock(cancellationToken); - clientConnection = newClientConnection; - } - } - catch - { - clientConnection.Release(); - throw; - } - return clientConnection; - } - private static ClientConnection GetOrAdd(IConnectionKey key) => Connections.GetOrAdd(key, key => key.CreateClientConnection()); - public static bool TryGet(IConnectionKey key, out ClientConnection connection) => Connections.TryGetValue(key, out connection); - internal static ClientConnection Remove(IConnectionKey connectionKey) - { - Connections.TryRemove(connectionKey, out var clientConnection); - return clientConnection; - } -} -interface IConnectionKey : IEquatable -{ - string SslServer { get; } - ClientConnection CreateClientConnection(); -} -abstract class ClientConnection : IDisposable -{ - readonly SemaphoreSlim _lock = new(1); - Connection _connection; - protected ClientConnection(IConnectionKey connectionKey) => ConnectionKey = connectionKey; - public abstract bool Connected { get; } - public Connection Connection - { - get => _connection; - set - { - _connection = value; - _connection.Closed += OnConnectionClosed; - } - } - public abstract Task Connect(CancellationToken cancellationToken); - private void OnConnectionClosed(object sender, EventArgs _) - { - var closedConnection = (Connection)sender; - if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out var clientConnection) || clientConnection.Connection != closedConnection) - { - return; - } - if (!clientConnection.TryLock()) - { - return; - } - try - { - if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out clientConnection) || clientConnection.Connection != closedConnection) - { - return; - } - var removedConnection = ClientConnectionsRegistry.Remove(ConnectionKey); - if (_connection.LogEnabled) - { - _connection.Log($"Remove connection {removedConnection}."); - } - Debug.Assert(removedConnection?.Connection == closedConnection, "Removed the wrong connection."); - } - finally - { - Release(); - } - } - public Server Server { get; set; } - protected IConnectionKey ConnectionKey { get; } - public Task Lock(CancellationToken cancellationToken = default) => _lock.WaitAsync(cancellationToken); - public void Release() => _lock.Release(); - public bool TryLock() => _lock.Wait(millisecondsTimeout: 0); - public override string ToString() => _connection?.Name ?? base.ToString(); - protected virtual void Dispose(bool disposing) => _lock.AssertDisposed(); - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Client/IpcProxy.cs b/src/UiPath.CoreIpc/Client/IpcProxy.cs new file mode 100644 index 00000000..9203b1a5 --- /dev/null +++ b/src/UiPath.CoreIpc/Client/IpcProxy.cs @@ -0,0 +1,21 @@ +namespace UiPath.Ipc; + +public class IpcProxy : DispatchProxy, IDisposable +{ + internal ServiceClient ServiceClient { get; set; } = null!; + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + => ServiceClient.Invoke(targetMethod!, args!); + + public void Dispose() => ServiceClient?.Dispose(); + + public ValueTask CloseConnection() => ServiceClient.CloseConnection(); + + public event EventHandler ConnectionClosed + { + add => ServiceClient.ConnectionClosed += value; + remove => ServiceClient.ConnectionClosed -= value; + } + + public Stream? Network => ServiceClient.Network; +} diff --git a/src/UiPath.CoreIpc/Client/ServiceClient.cs b/src/UiPath.CoreIpc/Client/ServiceClient.cs index 8ee847f5..bdde0ae7 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -1,138 +1,106 @@ -using System.Net.Security; -namespace UiPath.CoreIpc; +namespace UiPath.Ipc; -using ConnectionFactory = Func>; -using BeforeCallHandler = Func; -using InvokeDelegate = Func; - -interface IServiceClient : IDisposable +internal abstract class ServiceClient : IDisposable { - Task Invoke(MethodInfo method, object[] args); - Connection Connection { get; } -} + private static readonly MethodInfo GenericDefOf_CreateProxy = ((Func)CreateProxy).Method.GetGenericMethodDefinition(); -class ServiceClient : IServiceClient, IConnectionKey where TInterface : class -{ - private readonly ISerializer _serializer; - private readonly TimeSpan _requestTimeout; - private readonly ILogger _logger; - private readonly ConnectionFactory _connectionFactory; - private readonly BeforeCallHandler _beforeCall; - private readonly EndpointSettings _serviceEndpoint; - private readonly SemaphoreSlim _connectionLock = new(1); - private Connection _connection; - private Server _server; - private ClientConnection _clientConnection; - - internal ServiceClient(ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, string sslServer = null, BeforeCallHandler beforeCall = null, bool objectParameters = false, EndpointSettings serviceEndpoint = null) + private static IpcProxy CreateProxy(ServiceClient serviceClient) where T : class { - ObjectParameters = objectParameters; - _serializer = serializer; - _requestTimeout = requestTimeout; - _logger = logger; - _connectionFactory = connectionFactory; - SslServer = sslServer; - _beforeCall = beforeCall; - _serviceEndpoint = serviceEndpoint; + var proxy = (DispatchProxy.Create() as IpcProxy)!; + proxy.ServiceClient = serviceClient; + return proxy; } - protected int HashCode { get; init; } - public string SslServer { get; init; } - public virtual string Name => _connection?.Name; - private bool LogEnabled => _logger.Enabled(); - Connection IServiceClient.Connection => _connection; - public bool ObjectParameters { get; init; } - - public TInterface CreateProxy() + + protected abstract IClientConfig Config { get; } + public abstract Stream? Network { get; } + public event EventHandler? ConnectionClosed; + + private readonly Type _interfaceType; + private readonly Lazy _proxy; + + protected ServiceClient(Type interfaceType) { - var proxy = DispatchProxy.Create(); - (proxy as IpcProxy).ServiceClient = this; - return proxy; + _interfaceType = interfaceType; + _proxy = new(() => (GenericDefOf_CreateProxy.MakeGenericMethod(interfaceType).Invoke(null, [this]) as IpcProxy)!); } - - public override int GetHashCode() => HashCode; - private void OnNewConnection(Connection connection, bool alreadyHasServer = false) + protected void RaiseConnectionClosed() => ConnectionClosed?.Invoke(this, EventArgs.Empty); + public virtual ValueTask CloseConnection() => throw new NotSupportedException(); + public object? Invoke(MethodInfo method, object?[] args) => GetInvokeDelegate(method.ReturnType)(this, method, args); + + protected abstract Task<(Connection connection, bool newlyConnected)> EnsureConnection(CancellationToken ct); + + public T GetProxy() where T : class { - _connection?.Dispose(); - _connection = connection; - if (alreadyHasServer || _serviceEndpoint == null) + if (!typeof(T).IsAssignableFrom(_interfaceType)) { - return; + throw new ArgumentOutOfRangeException($"The provided generic argument T is not assignable to the proxy type. T is {typeof(T).Name}. The proxy type is {_interfaceType.Name}."); } - connection.Logger ??= _logger; - var endpoints = new ConcurrentDictionary { [_serviceEndpoint.Name] = _serviceEndpoint }; - var listenerSettings = new ListenerSettings(Name) { RequestTimeout = _requestTimeout, ServiceProvider = _serviceEndpoint.ServiceProvider, Endpoints = endpoints }; - _server = new(listenerSettings, connection); + + return (_proxy.Value as T)!; } - public Task Invoke(MethodInfo method, object[] args) + private Task Invoke(MethodInfo method, object?[] args) { - var syncContext = SynchronizationContext.Current; - var defaultContext = syncContext == null || syncContext.GetType() == typeof(SynchronizationContext); + var sc = SynchronizationContext.Current; + var defaultContext = + (sc is null || sc.GetType() == typeof(SynchronizationContext)) && + TaskScheduler.Current == TaskScheduler.Default; + return defaultContext ? Invoke() : Task.Run(Invoke); + async Task Invoke() { CancellationToken cancellationToken = default; TimeSpan messageTimeout = default; - TimeSpan clientTimeout = _requestTimeout; - Stream uploadStream = null; - string[] serializedArguments = null; + TimeSpan clientTimeout = Config.RequestTimeout; + Stream? uploadStream = null; var methodName = method.Name; - SerializeArguments(); - var timeoutHelper = new TimeoutHelper(clientTimeout, cancellationToken); + + var serializedArguments = SerializeArguments(); + + using var timeoutHelper = new TimeoutHelper(clientTimeout, cancellationToken); try { - var token = timeoutHelper.Token; - bool newConnection; - await _connectionLock.WaitAsync(token); - try - { - newConnection = await EnsureConnection(token); - } - finally - { - _connectionLock.Release(); - } - if (_beforeCall != null) - { - await _beforeCall(new(newConnection, method, args), token); - } - var requestId = _connection.NewRequestId(); - var request = new Request(typeof(TInterface).Name, requestId, methodName, serializedArguments, ObjectParameters ? args : null, messageTimeout.TotalSeconds) + var ct = timeoutHelper.Token; + + var (connection, newConnection) = await EnsureConnection(ct); + + await (Config.BeforeOutgoingCall?.Invoke(new CallInfo(newConnection, method, args), ct) ?? Task.CompletedTask); + + var requestId = connection.NewRequestId(); + var request = new Request(_interfaceType.Name, requestId, methodName, serializedArguments, messageTimeout.TotalSeconds) { UploadStream = uploadStream }; - if (LogEnabled) - { - Log($"IpcClient calling {methodName} {requestId} {Name}."); - } - if (ObjectParameters && !method.ReturnType.IsGenericType) + + Config.Logger?.ServiceClient_Calling(methodName, requestId, Config.GetComputedDebugName()); + + Response response; + try { - await _connection.Send(request, token); - return default; + response = await connection.RemoteCall(request, ct); // returns user errors instead of throwing them (could throw for system bugs) + + Config.Logger?.ServiceClient_CalledSuccessfully(request.MethodName, requestId, Config.GetComputedDebugName()); } - var response = await _connection.RemoteCall(request, token); - if (LogEnabled) + catch (Exception ex) { - Log($"IpcClient called {methodName} {requestId} {Name}."); + Config.Logger?.ServiceClient_FailedToCall(request.MethodName, requestId, Config.GetComputedDebugName(), ex); + throw; } - return response.Deserialize(_serializer, ObjectParameters); + + return response.Deserialize(); } catch (Exception ex) { timeoutHelper.ThrowTimeout(ex, methodName); throw; } - finally - { - timeoutHelper.Dispose(); - } - void SerializeArguments() + + string[] SerializeArguments() { - if (!ObjectParameters) - { - serializedArguments = new string[args.Length]; - } + var result = new string[args.Length]; + for (int index = 0; index < args.Length; index++) { switch (args[index]) @@ -150,168 +118,170 @@ void SerializeArguments() args[index] = ""; break; } - if (!ObjectParameters) - { - serializedArguments[index] = _serializer.Serialize(args[index]); - } - } - } - } - } - private async Task EnsureConnection(CancellationToken cancellationToken) - { - if (_connectionFactory != null) - { - var externalConnection = await _connectionFactory(_connection, cancellationToken); - if (externalConnection != null) - { - if (_connection == null) - { - OnNewConnection(externalConnection); - return true; + result[index] = IpcJsonSerializer.Instance.Serialize(args[index]); } - return false; + + return result; } } - if (_clientConnection?.Connected is true) - { - return false; - } - return await Connect(cancellationToken); } - private async Task Connect(CancellationToken cancellationToken) + public abstract void Dispose(); + + public override string ToString() => Config.GetComputedDebugName(); + + #region Generic adapter cache + private static readonly MethodInfo GenericDefOf_Invoke = ((Func>)Invoke).Method.GetGenericMethodDefinition(); + private static readonly ConcurrentDictionary ReturnTypeToInvokeDelegate = new(); + private static InvokeDelegate GetInvokeDelegate(Type returnType) => ReturnTypeToInvokeDelegate.GetOrAdd(returnType, CreateInvokeDelegate); + private static InvokeDelegate CreateInvokeDelegate(Type returnType) + => GenericDefOf_Invoke.MakeGenericDelegate( + returnType.IsGenericType + ? returnType.GetGenericArguments()[0] + : typeof(object)); + + private static Task Invoke(ServiceClient serviceClient, MethodInfo method, object?[] args) => serviceClient.Invoke(method, args); + #endregion +} + +internal sealed class ServiceClientProper : ServiceClient +{ + private readonly FastAsyncLock _lock = new(); + private readonly IpcClient _client; + private readonly IClientState _clientState; + + private Connection? _latestConnection; + private Server? _latestServer; + + private Connection? LatestConnection { - var clientConnection = await ClientConnectionsRegistry.GetOrCreate(this, cancellationToken); - try + get => _latestConnection; + set { - if (clientConnection.Connected) + if (_latestConnection == value) { - ReuseClientConnection(clientConnection); - return false; + return; } - clientConnection.Dispose(); - Stream network; - try - { - network = await clientConnection.Connect(cancellationToken); - } - catch - { - clientConnection.Dispose(); - throw; - } - var stream = SslServer == null ? network : await AuthenticateAsClient(network); - OnNewConnection(new(stream, _serializer, _logger, Name)); - if (LogEnabled) - { - Log($"CreateConnection {Name}."); - } - InitializeClientConnection(clientConnection); - } - finally - { - clientConnection.Release(); - } - return true; - async Task AuthenticateAsClient(Stream network) - { - var sslStream = new SslStream(network); - try - { - await sslStream.AuthenticateAsClientAsync(SslServer); - } - catch + + if (_latestConnection is not null) { - sslStream.Dispose(); - throw; + _latestConnection.Closed -= LatestConnection_Closed; } - Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); - return sslStream; - } - } - private void ReuseClientConnection(ClientConnection clientConnection) - { - _clientConnection = clientConnection; - var alreadyHasServer = clientConnection.Server != null; - if (LogEnabled) - { - Log(nameof(ReuseClientConnection) + " " + clientConnection); - } - OnNewConnection(clientConnection.Connection, alreadyHasServer); - if (!alreadyHasServer) - { - clientConnection.Server = _server; - } - else if (_serviceEndpoint != null) - { - _server = clientConnection.Server; - if (_server.Endpoints.ContainsKey(_serviceEndpoint.Name)) + _latestConnection = value; + + if (_latestConnection is not null) { - throw new InvalidOperationException($"Duplicate callback proxy instance {Name} <{typeof(TInterface).Name}, {_serviceEndpoint.Contract.Name}>. Consider using a singleton callback proxy."); + _latestConnection.Closed += LatestConnection_Closed; } - _server.Endpoints.Add(_serviceEndpoint.Name, _serviceEndpoint); } } - public void Log(string message) => _logger.LogInformation(message); + public override Stream? Network => LatestConnection?.Network; - private void InitializeClientConnection(ClientConnection clientConnection) + public ServiceClientProper(IpcClient client, Type interfaceType) : base(interfaceType) { - _connection.Listen().LogException(_logger, Name); - clientConnection.Connection = _connection; - clientConnection.Server = _server; - _clientConnection = clientConnection; + _client = client; + client.Transport.Validate(); + _clientState = client.Transport.CreateState(); } - public void Dispose() + public override void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + CloseConnection().AsTask().TraceError(); } - protected virtual void Dispose(bool disposing) + public override async ValueTask CloseConnection() { - _connectionLock.AssertDisposed(); - if (LogEnabled) + using (await _lock.Lock()) { - Log($"Dispose {Name}"); + LatestConnection?.Dispose(); + LatestConnection = null; } - if (disposing) + } + + private void LatestConnection_Closed(object? sender, EventArgs e) => RaiseConnectionClosed(); + + protected override async Task<(Connection connection, bool newlyConnected)> EnsureConnection(CancellationToken ct) + { + + using (await _lock.Lock(ct)) { - _server?.Endpoints.Remove(_serviceEndpoint.Name); + var haveConnectionAlready = LatestConnection is not null; + var isConnected = new Lazy(_clientState.IsConnected); + var haveBeforeConnect = Config.BeforeConnect is not null; + + if (haveConnectionAlready && isConnected.Value) + { + return (LatestConnection!, newlyConnected: false); + } + + if (haveBeforeConnect) + { + await Config.BeforeConnect!(ct); + } + + var network = await Connect(ct); + + LatestConnection = new Connection(network, Config.GetComputedDebugName(), Config.Logger); + var router = new Router(_client.CreateCallbackRouterConfig(), _client.ServiceProvider); + _latestServer = new Server(router, _client.RequestTimeout, LatestConnection); + + _ = Pal(); + return (LatestConnection, newlyConnected: true); + + async Task Pal() + { + try + { + await LatestConnection.Listen(); + } + catch (Exception ex) + { + Config.Logger.LogException(ex, Config.GetComputedDebugName()); + } + } } } - public override string ToString() => Name; + private async Task Connect(CancellationToken ct) + { + await _clientState.Connect(_client, ct); + + if (_clientState.Network is not { } network) + { + throw new InvalidOperationException(); + } - public virtual bool Equals(IConnectionKey other) => SslServer == other.SslServer; + return network; + } - public virtual ClientConnection CreateClientConnection() => throw new NotImplementedException(); + protected override IClientConfig Config => _client; } -public class IpcProxy : DispatchProxy, IDisposable +internal sealed class ServiceClientForCallback : ServiceClient where TInterface : class { - private static readonly MethodInfo InvokeMethod = typeof(IpcProxy).GetStaticMethod(nameof(GenericInvoke)); - private static readonly ConcurrentDictionary InvokeByType = new(); - - internal IServiceClient ServiceClient { get; set; } + private readonly Connection _connection; + private readonly IClientConfig _config; - public Connection Connection => ServiceClient.Connection; + public override Stream? Network => _connection.Network; - protected override object Invoke(MethodInfo targetMethod, object[] args) => GetInvoke(targetMethod)(ServiceClient, targetMethod, args); - - public void Dispose() => ServiceClient.Dispose(); + public ServiceClientForCallback(Connection connection, IClientConfig config) : base(typeof(TInterface)) + { + _connection = connection; + _config = config; + } - public void CloseConnection() => Connection?.Dispose(); + public TInterface GetProxy() => GetProxy(); - private static InvokeDelegate GetInvoke(MethodInfo targetMethod) => InvokeByType.GetOrAdd(targetMethod.ReturnType, taskType => + public override void Dispose() { - var resultType = taskType.IsGenericType ? taskType.GenericTypeArguments[0] : typeof(object); - return InvokeMethod.MakeGenericDelegate(resultType); - }); + // do nothing + } + + protected override Task<(Connection connection, bool newlyConnected)> EnsureConnection(CancellationToken ct) + => Task.FromResult((_connection, newlyConnected: false)); - private static object GenericInvoke(IServiceClient serviceClient, MethodInfo method, object[] args) => serviceClient.Invoke(method, args); -} \ No newline at end of file + protected override IClientConfig Config => _config; +} diff --git a/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs b/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs deleted file mode 100644 index db59c9f7..00000000 --- a/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace UiPath.CoreIpc; - -using ConnectionFactory = Func>; -using BeforeCallHandler = Func; - -public abstract class ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder -{ - protected readonly IServiceProvider _serviceProvider; - protected ISerializer _serializer = new IpcJsonSerializer(); - protected TimeSpan _requestTimeout = Timeout.InfiniteTimeSpan; - protected ILogger _logger; - protected ConnectionFactory _connectionFactory; - protected BeforeCallHandler _beforeCall; - protected object _callbackInstance; - protected TaskScheduler _taskScheduler; - protected string _sslServer; - protected bool _objectParameters; - - protected ServiceClientBuilder(Type callbackContract, IServiceProvider serviceProvider) - { - CallbackContract = callbackContract; - _serviceProvider = serviceProvider; - } - - internal Type CallbackContract { get; } - - public TDerived DontReconnect() => ConnectionFactory((connection, _) => Task.FromResult(connection)); - - public TDerived ConnectionFactory(ConnectionFactory connectionFactory) - { - _connectionFactory = connectionFactory; - return (TDerived)this; - } - - public TDerived EncryptAndSign(string certificateServerName) - { - if (string.IsNullOrWhiteSpace(certificateServerName)) - { - throw new ArgumentException($"'{nameof(certificateServerName)}' must match the name on the server's certificate.", nameof(certificateServerName)); - } - _sslServer = certificateServerName; - return (TDerived)this; - } - - public TDerived BeforeCall(BeforeCallHandler beforeCall) - { - _beforeCall = beforeCall; - return (TDerived)this; - } - - public TDerived Logger(ILogger logger) - { - _logger = logger; - return (TDerived)this; - } - /// - /// By default, method parameters are serialized as json strings. Setting this allows serialization as json objects. - /// This should improve performance for large strings, but decrease it for many small objects. - /// Setting it breaks compatibility with older servers. - /// So a proxy with this setting will only be able to connect to servers that understand the new encoding. - /// - /// this - public TDerived SerializeParametersAsObjects() - { - _objectParameters = true; - return (TDerived)this; - } - - public TDerived Logger(IServiceProvider serviceProvider) => Logger(serviceProvider.GetRequiredService>()); - - public TDerived Serializer(ISerializer serializer) - { - _serializer = serializer; - return (TDerived) this; - } - - public TDerived RequestTimeout(TimeSpan timeout) - { - _requestTimeout = timeout; - return (TDerived) this; - } - - protected abstract TInterface BuildCore(EndpointSettings serviceEndpoint); - - public TInterface Build() - { - if (CallbackContract == null) - { - return BuildCore(null); - } - if (_logger == null) - { - Logger(_serviceProvider); - } - return BuildCore(new(CallbackContract, _callbackInstance) { Scheduler = _taskScheduler, ServiceProvider = _serviceProvider }); - } -} - -public readonly struct CallInfo -{ - public CallInfo(bool newConnection, MethodInfo method, object[] arguments) - { - NewConnection = newConnection; - Method = method; - Arguments = arguments; - } - public bool NewConnection { get; } - public MethodInfo Method { get; } - public object[] Arguments { get; } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Config/ClientTransport.cs b/src/UiPath.CoreIpc/Config/ClientTransport.cs new file mode 100644 index 00000000..1ef3942d --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ClientTransport.cs @@ -0,0 +1,10 @@ +namespace UiPath.Ipc; + +public abstract record ClientTransport +{ + private protected ClientTransport() { } + + internal abstract IClientState CreateState(); + + internal abstract void Validate(); +} diff --git a/src/UiPath.CoreIpc/Config/ContractCollection.cs b/src/UiPath.CoreIpc/Config/ContractCollection.cs new file mode 100644 index 00000000..0e1b3252 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ContractCollection.cs @@ -0,0 +1,20 @@ +using System.Collections; + +namespace UiPath.Ipc; + +public class ContractCollection : IEnumerable +{ + internal readonly Dictionary Endpoints = new(); + + public void Add(Type contractType) => Add(contractType, instance: null); + public void Add(Type contractType, object? instance) => Add(new ContractSettings(contractType, instance)); + public void Add(ContractSettings endpointSettings) + { + if (endpointSettings is null) throw new ArgumentNullException(nameof(endpointSettings)); + + Endpoints[endpointSettings.Service.Type] = endpointSettings; + } + public IEnumerator GetEnumerator() => Endpoints.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/UiPath.CoreIpc/Config/IClientConfig.cs b/src/UiPath.CoreIpc/Config/IClientConfig.cs new file mode 100644 index 00000000..9e4741ab --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IClientConfig.cs @@ -0,0 +1,11 @@ +namespace UiPath.Ipc; + +// Maybe decommission +internal interface IClientConfig +{ + TimeSpan RequestTimeout { get; } + BeforeConnectHandler? BeforeConnect { get; } + BeforeCallHandler? BeforeOutgoingCall { get; } + ILogger? Logger { get; } + string GetComputedDebugName(); +} diff --git a/src/UiPath.CoreIpc/Config/IClientState.cs b/src/UiPath.CoreIpc/Config/IClientState.cs new file mode 100644 index 00000000..a09230cf --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IClientState.cs @@ -0,0 +1,9 @@ +namespace UiPath.Ipc; + +internal interface IClientState : IDisposable +{ + Stream? Network { get; } + + bool IsConnected(); + ValueTask Connect(IpcClient client, CancellationToken ct); +} diff --git a/src/UiPath.CoreIpc/Config/IpcBase.cs b/src/UiPath.CoreIpc/Config/IpcBase.cs new file mode 100644 index 00000000..1c03b561 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IpcBase.cs @@ -0,0 +1,8 @@ +namespace UiPath.Ipc; + +public abstract class IpcBase +{ + public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; + public IServiceProvider? ServiceProvider { get; set; } + public TaskScheduler? Scheduler { get; set; } +} diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs new file mode 100644 index 00000000..4e89198d --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -0,0 +1,36 @@ +namespace UiPath.Ipc; + +public sealed class IpcClient : IpcBase, IClientConfig +{ + public ContractCollection? Callbacks { get; set; } + + public ILogger? Logger { get; init; } + public BeforeConnectHandler? BeforeConnect { get; set; } + public BeforeCallHandler? BeforeOutgoingCall { get; set; } + + internal string DebugName { get; set; } = null!; + + public required ClientTransport Transport { get; init; } + + string IClientConfig.GetComputedDebugName() => DebugName ?? Transport.ToString(); + + private readonly ConcurrentDictionary _clients = new(); + private ServiceClient GetServiceClient(Type proxyType) + { + return _clients.GetOrAdd(proxyType, Create); + + ServiceClient Create(Type proxyType) => new ServiceClientProper(this, proxyType); + } + public TProxy GetProxy() where TProxy : class => GetServiceClient(typeof(TProxy)).GetProxy(); + + internal RouterConfig CreateCallbackRouterConfig() + => RouterConfig.From( + Callbacks.OrDefault(), + endpoint => + { + var clone = new ContractSettings(endpoint); + clone.BeforeIncomingCall = null; // callbacks don't support BeforeIncomingCall + clone.Scheduler ??= Scheduler; + return clone; + }); +} diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs new file mode 100644 index 00000000..dc94b5c1 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -0,0 +1,194 @@ +using System.Diagnostics.CodeAnalysis; + +namespace UiPath.Ipc; + +public sealed class IpcServer : IpcBase, IAsyncDisposable +{ + public required ContractCollection Endpoints { get; init; } + public required ServerTransport Transport { get; init; } + + private readonly object _lock = new(); + private readonly TaskCompletionSource _listening = new(); + private readonly CancellationTokenSource _ctsActiveConnections = new(); + + private bool _disposeStarted; + private Accepter? _accepter; + private Lazy _dispose; + + public IpcServer() + { + _dispose = new(DisposeCore); + } + + public ValueTask DisposeAsync() => new(_dispose.Value); + + private async Task DisposeCore() + { + Accepter? accepter = null; + lock (_lock) + { + _disposeStarted = true; + accepter = _accepter; + } + + await (accepter?.DisposeAsync() ?? default); + _ctsActiveConnections.Cancel(); + _ctsActiveConnections.Dispose(); + } + + public void Start() + { + lock (_lock) + { + if (_disposeStarted) + { + throw new ObjectDisposedException(nameof(IpcServer)); + } + + if (!IsValid(out var errors)) + { + throw new InvalidOperationException($"ValidationErrors:\r\n{string.Join("\r\n", errors)}"); + } + + if (_accepter is not null) + { + return; + } + + _accepter = new(Transport, new ObserverAdapter() + { + OnNext = OnNewConnection, + OnError = OnNewConnectionError, + }); + } + } + + public Task WaitForStart() + { + Start(); + return _accepter!.StartedAccepting; + } + + internal ILogger? CreateLogger(string category) => ServiceProvider.MaybeCreateLogger(category); + + private void OnNewConnection(Stream network) + { + ServerConnection.CreateAndListen(server: this, network, ct: _ctsActiveConnections.Token); + } + + private void OnNewConnectionError(Exception ex) + { + Trace.TraceError($"Failed to accept new connection. Ex: {ex}"); + } + + internal RouterConfig CreateRouterConfig(IpcServer server) => RouterConfig.From( + server.Endpoints, + endpoint => + { + var clone = new ContractSettings(endpoint); + clone.Scheduler ??= server.Scheduler; + return clone; + }); + + private sealed class ObserverAdapter : IObserver + { + public required Action OnNext { get; init; } + public Action? OnError { get; init; } + public Action? OnCompleted { get; init; } + + void IObserver.OnNext(T value) => OnNext(value); + void IObserver.OnError(Exception error) => OnError?.Invoke(error); + void IObserver.OnCompleted() => OnCompleted?.Invoke(); + } + + private sealed class Accepter : IAsyncDisposable + { + private readonly CancellationTokenSource _cts = new(); + private readonly ServerTransport.IServerState _serverState; + private readonly Task _running; + private readonly IObserver _newConnection; + private readonly TaskCompletionSource _tcsStartedAccepting = new(); + private readonly Lazy _dispose; + + public Task StartedAccepting => _tcsStartedAccepting.Task; + + public Accepter(ServerTransport transport, IObserver connected) + { + _serverState = transport.CreateServerState(); + _newConnection = connected; + _running = RunOnThreadPool(LoopAccept, parallelCount: transport.ConcurrentAccepts, _cts.Token); + _dispose = new(DisposeCore); + } + + public ValueTask DisposeAsync() => new(_dispose.Value); + + private async Task DisposeCore() + { + _cts.Cancel(); + await _running; + _cts.Dispose(); + } + + + private async Task LoopAccept(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + await TryAccept(ct); /// this method doesn't throw, and in case of non- exceptions, + /// it will notify the observer. + } + + _newConnection.OnCompleted(); + } + + /// + /// This method returns when a new connection is accepted, or when cancellation or another error occurs. + /// In case of cancellation or error, it will dispose of the underlying resources and will suppress the exception. + /// In case of an error (not a cancellation), it will notify the observer about the error. + /// + private async Task TryAccept(CancellationToken ct) + { + var slot = _serverState.CreateConnectionSlot(); + + try + { + var taskNewConnection = slot.AwaitConnection(ct); + _tcsStartedAccepting.TrySetResult(null); + + var newConnection = await taskNewConnection; + _newConnection.OnNext(newConnection); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == ct) + { + await slot.DisposeAsync(); + } + catch (Exception ex) + { + await slot.DisposeAsync(); + _newConnection.OnError(ex); + } + } + + private static Task RunOnThreadPool(Func action, int parallelCount, CancellationToken ct) + => Task.WhenAll(Enumerable.Range(start: 0, parallelCount).Select(_ => Task.Run(() => action(ct)))); + } + + [MemberNotNullWhen(returnValue: true, member: nameof(Transport))] + private bool IsValid([NotNullWhen(returnValue: false)] out string? errorMessage) + { + if (Transport is null) + { + errorMessage = $"{nameof(Transport)} is not set."; + return false; + } + + if (string.Join("\r\n", Transport.Validate()) is { Length: > 0 } concatenation) + { + errorMessage = concatenation; + return false; + } + + errorMessage = null; + return true; + } +} diff --git a/src/UiPath.CoreIpc/Config/ServerTransport.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs new file mode 100644 index 00000000..8c66a7e7 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -0,0 +1,63 @@ +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace UiPath.Ipc; + +public abstract class ServerTransport +{ + private protected ServerTransport() { } + + public int ConcurrentAccepts { get; set; } = 5; + public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; + + // TODO: Will be decommissioned altogether. + internal X509Certificate? Certificate { get; init; } + + internal int MaxMessageSize => MaxReceivedMessageSizeInMegabytes * 1024 * 1024; + + // TODO: Maybe decommission. + internal async Task MaybeAuthenticate(Stream network) + { + if (Certificate is null) + { + return network; + } + + var sslStream = new SslStream(network, leaveInnerStreamOpen: false); + try + { + await sslStream.AuthenticateAsServerAsync(Certificate); + } + catch + { + sslStream.Dispose(); + throw; + } + + Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); + return sslStream; + } + + internal abstract IServerState CreateServerState(); + + internal IEnumerable Validate() + => ValidateCore().Where(x => x is not null).Select(x => $"{GetType().Name}.{x}"); + internal abstract IEnumerable ValidateCore(); + internal static string? IsNotNull(T? propertyValue, [CallerArgumentExpression(nameof(propertyValue))] string? propertyName = null) + { + if (propertyValue is null) + { + return $"{propertyName} is required."; + } + return null; + } + + internal interface IServerState : IAsyncDisposable + { + IServerConnectionSlot CreateConnectionSlot(); + } + internal interface IServerConnectionSlot : IAsyncDisposable + { + ValueTask AwaitConnection(CancellationToken ct); + } +} diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index e4404147..f401a759 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -1,7 +1,12 @@ -namespace UiPath.CoreIpc; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipes; + +namespace UiPath.Ipc; + using static TaskCompletionPool; using static IOHelpers; -public sealed class Connection : IDisposable + +internal sealed class Connection : IDisposable { private static readonly IOException ClosedException = new("Connection closed."); private readonly ConcurrentDictionary _requests = new(); @@ -12,38 +17,43 @@ public sealed class Connection : IDisposable private readonly WaitCallback _onResponse; private readonly WaitCallback _onRequest; private readonly WaitCallback _onCancellation; - private readonly Action _cancelRequest; + private readonly Action _cancelRequest; private readonly byte[] _buffer = new byte[sizeof(long)]; private readonly NestedStream _nestedStream; - public Connection(Stream network, ISerializer serializer, ILogger logger, string name, int maxMessageSize = int.MaxValue) + + public string DebugName { get; } + public ILogger? Logger { get; } + + public Stream Network { get; } + + [MemberNotNullWhen(returnValue: true, nameof(Logger))] + public bool LogEnabled => Logger.Enabled(); + + public Connection(Stream network, string debugName, ILogger? logger, int maxMessageSize = int.MaxValue) { Network = network; _nestedStream = new NestedStream(network, 0); - Serializer = serializer; + DebugName = debugName; Logger = logger; - Name = $"{name} {GetHashCode()}"; _maxMessageSize = maxMessageSize; + _onResponse = response => OnResponseReceived((Response)response!); + _onRequest = request => OnRequestReceived((Request)request!); + _onCancellation = requestId => OnCancellationReceived((CancellationRequest)requestId!); + _cancelRequest = requestId => CancelRequest((string)requestId!); _receiveLoop = new(ReceiveLoop); - _onResponse = response => OnResponseReceived((Response)response); - _onRequest = request => OnRequestReceived((Request)request); - _onCancellation = requestId => OnCancellationReceived((string)requestId); - _cancelRequest = requestId => CancelRequest((string)requestId); } - public Stream Network { get; } - public ILogger Logger { get; internal set; } - public bool LogEnabled => Logger.Enabled(); - public string Name { get; } - public ISerializer Serializer { get; } - public override string ToString() => Name; + + public override string ToString() => DebugName; public string NewRequestId() => Interlocked.Increment(ref _requestCounter).ToString(); public Task Listen() => _receiveLoop.Value; - internal event Func RequestReceived; - internal event Action CancellationReceived; - public event EventHandler Closed; + + public event Func? RequestReceived; + public event Action? CancellationReceived; + public event EventHandler? Closed; #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] #endif - internal async ValueTask RemoteCall(Request request, CancellationToken token) + public async ValueTask RemoteCall(Request request, CancellationToken token) { var requestCompletion = Rent(); var requestId = request.Id; @@ -51,10 +61,13 @@ internal async ValueTask RemoteCall(Request request, CancellationToken var tokenRegistration = token.UnsafeRegister(_cancelRequest, requestId); try { + Logger?.LogInformation("Sending the request"); await Send(request, token); + Logger?.LogInformation("Sent the request"); } - catch + catch (Exception ex) { + Logger?.LogTrace($"Caught exception while sending the request. Ex: {ex}"); tokenRegistration.Dispose(); if (_requests.TryRemove(requestId, out _)) { @@ -64,7 +77,19 @@ internal async ValueTask RemoteCall(Request request, CancellationToken } try { - return await requestCompletion.ValueTask(); + Logger?.LogInformation("Waiting for the completion source to complete."); + Response response; + try + { + response = await requestCompletion.ValueTask(); + Logger?.LogInformation("The completion source completed successfully."); + } + catch (Exception ex) + { + Logger?.LogInformation($"The completion source failed. Ex: {ex}"); + throw; + } + return response; } finally { @@ -73,16 +98,16 @@ internal async ValueTask RemoteCall(Request request, CancellationToken requestCompletion.Return(); } } - internal ValueTask Send(Request request, CancellationToken token) + public ValueTask Send(Request request, CancellationToken token) { - Debug.Assert(request.Parameters == null || request.ObjectParameters == null); + Logger?.LogInformation("Connection.Send..."); var uploadStream = request.UploadStream; var requestBytes = SerializeToStream(request); return uploadStream == null ? SendMessage(MessageType.Request, requestBytes, token) : SendStream(MessageType.UploadRequest, requestBytes, uploadStream, token); } - void CancelRequest(string requestId) + private void CancelRequest(string requestId) { CancelServerCall(requestId).LogException(Logger, this); if (_requests.TryRemove(requestId, out var requestCompletion)) @@ -93,9 +118,8 @@ void CancelRequest(string requestId) Task CancelServerCall(string requestId) => SendMessage(MessageType.CancellationRequest, SerializeToStream(new CancellationRequest(requestId)), default).AsTask(); } - internal ValueTask Send(Response response, CancellationToken cancellationToken) + public ValueTask Send(Response response, CancellationToken cancellationToken) { - Debug.Assert(response.Data == null || response.ObjectData == null); var responseBytes = SerializeToStream(response); return response.DownloadStream == null ? SendMessage(MessageType.Response, responseBytes, cancellationToken) : @@ -120,7 +144,7 @@ private async ValueTask SendStream(MessageType messageType, Stream data, Stream CancellationTokenRegistration tokenRegistration = default; try { - tokenRegistration = cancellationToken.UnsafeRegister(state => ((Connection)state).Dispose(), this); + tokenRegistration = cancellationToken.UnsafeRegister(state => ((Connection)state!).Dispose(), this); await Network.WriteMessage(messageType, data, cancellationToken); await Network.WriteBuffer(BitConverter.GetBytes(userStream.Length), cancellationToken); const int DefaultCopyBufferSize = 81920; @@ -137,13 +161,18 @@ private async ValueTask SendStream(MessageType messageType, Stream data, Stream #endif private async ValueTask SendMessage(MessageType messageType, MemoryStream data, CancellationToken cancellationToken) { + Logger?.LogInformation("Connection.SendMessage: Awaiting the acquiring of the sendLock"); await _sendLock.WaitAsync(cancellationToken); + try { + Logger?.LogInformation($"Connection.SendMessage: sendLock was successfully aquired. Pushing the bytes onto the network. ByteCount: {data.Length}"); await Network.WriteMessage(messageType, data, CancellationToken.None); + Logger?.LogInformation("Connection.SendMessage: Successfully pushed the bytes."); } finally { + Logger?.LogInformation("Connection.SendMessage: Releasing the sendLock."); _sendLock.Release(); } } @@ -183,14 +212,32 @@ private async ValueTask ReadBuffer(int length) { int offset = 0; int toRead = length; + do { - var read = await Network.ReadAsync( + int read; + try + { + read = await Network.ReadAsync( #if NET461 - _buffer, offset, toRead); + _buffer, offset, toRead); #else - _buffer.AsMemory(offset, toRead)); + _buffer.AsMemory(offset, toRead)); #endif + } + catch (OperationCanceledException ex) when (Network is PipeStream) + { + // Originally we decided to throw this exception the 2nd time we caught it, but later it was discovered that the NodeJS runtime continuosly retries. + + // In some Windows client environments, OperationCanceledException is sporadically thrown on named pipe ReadAsync operation (ERROR_OPERATION_ABORTED on overlapped ReadFile) + // The cause has not yet been discovered(os specific, antiviruses, monitoring application), and we have implemented a retry system + // ROBO-3083 + + Logger.LogException(ex, $"Retrying ReadAsync for {Network.GetType()}"); + await Task.Delay(10); //Without this delay, on net framework can get OperationCanceledException on the second ReadAsync call + continue; + } + if (read == 0) { return false; @@ -207,8 +254,9 @@ private async Task ReceiveLoop() { while (await ReadBuffer(HeaderLength)) { - Debug.Assert(SynchronizationContext.Current == null); var length = BitConverter.ToInt32(_buffer, startIndex: 1); + + Debug.Assert(SynchronizationContext.Current is null); if (length > _maxMessageSize) { throw new InvalidDataException($"Message too large. The maximum message size is {_maxMessageSize / (1024 * 1024)} megabytes."); @@ -216,17 +264,16 @@ private async Task ReceiveLoop() _nestedStream.Reset(length); await HandleMessage(); } + Logger?.Connection_ReceiveLoopEndedSuccessfully(DebugName); } catch (Exception ex) { - Logger.LogException(ex, $"{nameof(ReceiveLoop)} {Name}"); - } - if (LogEnabled) - { - Log($"{nameof(ReceiveLoop)} {Name} finished."); + Logger?.Connection_ReceiveLoopFailed(DebugName, ex); } + Dispose(); return; + #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] #endif @@ -242,7 +289,7 @@ async ValueTask HandleMessage() RunAsync(_onRequest, await Deserialize()); break; case MessageType.CancellationRequest: - RunAsync(_onCancellation, (await Deserialize()).RequestId); + RunAsync(_onCancellation, await Deserialize()); break; case MessageType.UploadRequest: await OnUploadRequest(); @@ -259,10 +306,10 @@ async ValueTask HandleMessage() }; } } - static void RunAsync(WaitCallback callback, object state) => ThreadPool.UnsafeQueueUserWorkItem(callback, state); + static void RunAsync(WaitCallback callback, object? state) => ThreadPool.UnsafeQueueUserWorkItem(callback, state); private async Task OnDownloadResponse() { - var response = await Deserialize(); + var response = (await Deserialize())!; await EnterStreamMode(); var streamDisposed = new TaskCompletionSource(); EventHandler disposedHandler = delegate { streamDisposed.TrySetResult(true); }; @@ -280,12 +327,12 @@ private async Task OnDownloadResponse() } private async Task OnUploadRequest() { - var request = await Deserialize(); + var request = (await Deserialize())!; await EnterStreamMode(); using (_nestedStream) { request.UploadStream = _nestedStream; - await OnRequestReceived(request); + await OnRequestReceivedAsyncSafe(request); } } private async Task EnterStreamMode() @@ -303,7 +350,7 @@ private MemoryStream SerializeToStream(object value) try { stream.Position = HeaderLength; - Serializer.Serialize(value, stream); + IpcJsonSerializer.Instance.Serialize(value, stream); return stream; } catch @@ -312,30 +359,34 @@ private MemoryStream SerializeToStream(object value) throw; } } - private ValueTask Deserialize() => Serializer.DeserializeAsync(_nestedStream); - private void OnCancellationReceived(string requestId) + private ValueTask Deserialize() => IpcJsonSerializer.Instance.DeserializeAsync(_nestedStream, Logger); + + private void OnCancellationReceived(CancellationRequest cancellationRequest) { try { - CancellationReceived(requestId); + CancellationReceived?.Invoke(cancellationRequest.RequestId); } - catch(Exception ex) + catch (Exception ex) { Log(ex); } } - private void Log(Exception ex) => Logger.LogException(ex, Name); - private ValueTask OnRequestReceived(Request request) + private void OnRequestReceived(Request request) + { + _ = OnRequestReceivedAsyncSafe(request); + } + + private async Task OnRequestReceivedAsyncSafe(Request request) { try { - return RequestReceived(request); + await (RequestReceived?.Invoke(request) ?? default); } catch (Exception ex) { Log(ex); } - return default; } private void OnResponseReceived(Response response) { @@ -343,7 +394,7 @@ private void OnResponseReceived(Response response) { if (LogEnabled) { - Log($"Received response for request {response.RequestId} {Name}."); + Log($"Received response for request {response.RequestId} {DebugName}."); } if (_requests.TryRemove(response.RequestId, out var completionSource)) { @@ -355,5 +406,15 @@ private void OnResponseReceived(Response response) Log(ex); } } - public void Log(string message) => Logger.LogInformation(message); + + private void Log(Exception ex) => Logger?.LogException(ex, DebugName); + private void Log(string message) + { + if (Logger is null) + { + throw new InvalidOperationException(); + } + + Logger.LogInformation(message); + } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/GlobalSuppressions.cs b/src/UiPath.CoreIpc/GlobalSuppressions.cs index cee98987..ef17d024 100644 --- a/src/UiPath.CoreIpc/GlobalSuppressions.cs +++ b/src/UiPath.CoreIpc/GlobalSuppressions.cs @@ -6,9 +6,3 @@ [assembly: SuppressMessage("Performance", "HAA0505:Initializer reference type allocation", Scope = "module")] [assembly: SuppressMessage("Performance", "HAA0502:Explicit new reference type allocation", Scope = "module")] [assembly: SuppressMessage("Performance", "HAA0501:Explicit new array type allocation", Scope = "module")] -#if NET461 -namespace System.Runtime.CompilerServices; -internal static class IsExternalInit -{ -} -#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/GlobalUsings.cs b/src/UiPath.CoreIpc/GlobalUsings.cs new file mode 100644 index 00000000..4fd94c5b --- /dev/null +++ b/src/UiPath.CoreIpc/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using BeforeConnectHandler = System.Func; +global using BeforeCallHandler = System.Func; +global using InvokeDelegate = System.Func; +global using Accept = System.Func>; +global using ContractToSettingsMap = System.Collections.Generic.Dictionary; +global using AccessControlDelegate = System.Action; diff --git a/src/UiPath.CoreIpc/CancellationTokenSourcePool.cs b/src/UiPath.CoreIpc/Helpers/CancellationTokenSourcePool.cs similarity index 98% rename from src/UiPath.CoreIpc/CancellationTokenSourcePool.cs rename to src/UiPath.CoreIpc/Helpers/CancellationTokenSourcePool.cs index a6d0b3f8..0d517d86 100644 --- a/src/UiPath.CoreIpc/CancellationTokenSourcePool.cs +++ b/src/UiPath.CoreIpc/Helpers/CancellationTokenSourcePool.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc; +namespace UiPath.Ipc; // https://github.com/dotnet/aspnetcore/blob/main/src/Shared/CancellationTokenSourcePool.cs internal static class CancellationTokenSourcePool { diff --git a/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs new file mode 100644 index 00000000..82ebf122 --- /dev/null +++ b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace UiPath.Ipc; + +internal static class DefaultsExtensions +{ + public static ILogger? MaybeCreateLogger(this IServiceProvider? serviceProvider, string category) => serviceProvider?.GetService()?.CreateLogger(category); + + public static ILogger OrDefault(this ILogger? logger) => logger ?? NullLogger.Instance; + public static BeforeCallHandler OrDefault(this BeforeCallHandler? beforeCallHandler) => beforeCallHandler ?? DefaultBeforeCallHandler; + public static TaskScheduler OrDefault(this TaskScheduler? scheduler) => scheduler ?? TaskScheduler.Default; + public static ContractToSettingsMap OrDefault(this ContractToSettingsMap? map) => map ?? EmptyContractToSettingsMap; + public static ContractCollection OrDefault(this ContractCollection? endpoints) => endpoints ?? new(); + + public static Func? MaybeCreateServiceFactory(this IServiceProvider? serviceProvider) where T : class + { + if (serviceProvider is null) + { + return null; + } + + return serviceProvider.GetRequiredService; + } + + private static readonly BeforeCallHandler DefaultBeforeCallHandler = (_, _) => Task.CompletedTask; + + private static readonly ContractToSettingsMap EmptyContractToSettingsMap = new(); +} diff --git a/src/UiPath.CoreIpc/Helpers/FastAsyncLock.cs b/src/UiPath.CoreIpc/Helpers/FastAsyncLock.cs new file mode 100644 index 00000000..bfa2a64f --- /dev/null +++ b/src/UiPath.CoreIpc/Helpers/FastAsyncLock.cs @@ -0,0 +1,14 @@ +namespace UiPath.Ipc; + +internal sealed class FastAsyncLock : IDisposable +{ + private readonly SemaphoreSlim _semaphore = new(initialCount: 1, maxCount: 1); + + public async Task Lock(CancellationToken ct = default) + { + await _semaphore.WaitAsync(ct); + return this; + } + + public void Dispose() => _semaphore.Release(); +} diff --git a/src/UiPath.CoreIpc/Helpers.cs b/src/UiPath.CoreIpc/Helpers/Helpers.cs similarity index 76% rename from src/UiPath.CoreIpc/Helpers.cs rename to src/UiPath.CoreIpc/Helpers/Helpers.cs index 58c39029..84eac97c 100644 --- a/src/UiPath.CoreIpc/Helpers.cs +++ b/src/UiPath.CoreIpc/Helpers/Helpers.cs @@ -1,31 +1,25 @@ using Microsoft.IO; using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO.Pipes; -using System.Net; -using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; -namespace UiPath.CoreIpc; + +namespace UiPath.Ipc; + using static CancellationTokenSourcePool; -public static class Helpers + +internal static class Helpers { - public const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly; -#if NET461 - public static CancellationTokenRegistration UnsafeRegister(this CancellationToken token, Action callback, object state) => token.Register(callback, state); - public static async Task ConnectAsync(this TcpClient tcpClient, IPAddress address, int port, CancellationToken cancellationToken) - { - using var token = cancellationToken.Register(state => ((TcpClient)state).Dispose(), tcpClient); - await tcpClient.ConnectAsync(address, port); - } -#endif - public static Error ToError(this Exception ex) => new(ex.Message, ex.StackTrace ?? ex.GetBaseException().StackTrace, GetExceptionType(ex), ex.InnerException?.ToError()); - private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName; - public static bool Enabled(this ILogger logger) => logger != null && logger.IsEnabled(LogLevel.Information); + internal const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly; + internal static Error ToError(this Exception ex) => new(ex.Message, ex.StackTrace ?? ex.GetBaseException().StackTrace!, GetExceptionType(ex), ex.InnerException?.ToError()); + private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!; + internal static bool Enabled(this ILogger? logger, LogLevel logLevel = LogLevel.Information) => logger is not null && logger.IsEnabled(logLevel); [Conditional("DEBUG")] - public static void AssertDisposed(this SemaphoreSlim semaphore) => semaphore.AssertFieldNull("m_waitHandle"); + internal static void AssertDisposed(this SemaphoreSlim semaphore) => semaphore.AssertFieldNull("m_waitHandle"); [Conditional("DEBUG")] - public static void AssertDisposed(this CancellationTokenSource cts) + internal static void AssertDisposed(this CancellationTokenSource cts) { #if NET461 cts.AssertFieldNull("m_kernelEvent"); @@ -37,13 +31,12 @@ public static void AssertDisposed(this CancellationTokenSource cts) } [Conditional("DEBUG")] static void AssertFieldNull(this object obj, string field) => - Debug.Assert(obj.GetType().GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj) == null); - public static TDelegate MakeGenericDelegate(this MethodInfo genericMethod, Type genericArgument) where TDelegate : Delegate => + Debug.Assert(obj.GetType().GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(obj) is null); + internal static TDelegate MakeGenericDelegate(this MethodInfo genericMethod, Type genericArgument) where TDelegate : Delegate => (TDelegate)genericMethod.MakeGenericMethod(genericArgument).CreateDelegate(typeof(TDelegate)); - public static MethodInfo GetStaticMethod(this Type type, string name) => type.GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic); - public static MethodInfo GetInterfaceMethod(this Type type, string name) + internal static MethodInfo GetInterfaceMethod(this Type type, string name) { - var method = type.GetMethod(name, InstanceFlags) ?? + var method = type.GetMethod(name, InstanceFlags) ?? type.GetInterfaces().Select(t => t.GetMethod(name, InstanceFlags)).FirstOrDefault(m => m != null) ?? throw new ArgumentOutOfRangeException(nameof(name), name, $"Method '{name}' not found in interface '{type}'."); if (method.IsGenericMethod) @@ -52,28 +45,47 @@ public static MethodInfo GetInterfaceMethod(this Type type, string name) } return method; } - public static IEnumerable GetInterfaceMethods(this Type type) => + internal static IEnumerable GetInterfaceMethods(this Type type) => type.GetMethods().Concat(type.GetInterfaces().SelectMany(i => i.GetMethods())); - public static object GetDefaultValue(this ParameterInfo parameter) => parameter switch + internal static object? GetDefaultValue(this ParameterInfo parameter) => parameter switch { { HasDefaultValue: false } => null, { ParameterType: { IsValueType: true }, DefaultValue: null } => Activator.CreateInstance(parameter.ParameterType), _ => parameter.DefaultValue }; - public static ReadOnlyDictionary ToReadOnlyDictionary(this IDictionary dictionary) => new(dictionary); - public static void LogException(this ILogger logger, Exception ex, object tag) + internal static ReadOnlyDictionary ToReadOnlyDictionary(this IDictionary dictionary) where TKey : notnull => new(dictionary); + internal static void LogException(this ILogger? logger, Exception ex, object tag) { var message = $"{tag} # {ex}"; - if (logger != null) + + if (logger is not null) { logger.LogError(message); + return; } - else + + Trace.TraceError(message); + } + + internal static void TraceError(this Task task) + { + task.ContinueWith(task => + { + Trace.TraceError(task.Exception!.ToString()); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + + internal static void LogException(this Task task, ILogger? logger, object tag) => task.ContinueWith(result => logger.LogException(result.Exception!, tag), TaskContinuationOptions.NotOnRanToCompletion); + + internal static void WaitAndUnwrapException(this Task task) + { + if (task is null) { - Trace.TraceError(message); + throw new ArgumentNullException(nameof(task)); } + + task.GetAwaiter().GetResult(); } - public static void LogException(this Task task, ILogger logger, object tag) => task.ContinueWith(result => logger.LogException(result.Exception, tag), TaskContinuationOptions.NotOnRanToCompletion); } public static class IOHelpers { @@ -81,7 +93,7 @@ public static class IOHelpers private static readonly RecyclableMemoryStreamManager Pool = new(MaxBytes, MaxBytes); internal static MemoryStream GetStream(int size = 0) => Pool.GetStream("IpcMessage", size); internal const int HeaderLength = sizeof(int) + 1; - internal static NamedPipeServerStream NewNamedPipeServerStream(string pipeName, PipeDirection direction, int maxNumberOfServerInstances, PipeTransmissionMode transmissionMode, PipeOptions options, Func pipeSecurity) + internal static NamedPipeServerStream NewNamedPipeServerStream(string pipeName, PipeDirection direction, int maxNumberOfServerInstances, PipeTransmissionMode transmissionMode, PipeOptions options, Func pipeSecurity) { #if NET461 return new(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options, inBufferSize: 0, outBufferSize: 0, pipeSecurity()); @@ -114,17 +126,20 @@ public static PipeSecurity Allow(this PipeSecurity pipeSecurity, IdentityReferen public static PipeSecurity AllowCurrentUser(this PipeSecurity pipeSecurity, bool onlyNonAdmin = false) { - using (var currentIdentity = WindowsIdentity.GetCurrent()) + using (var currentIdentity = WindowsIdentity.GetCurrent()!) { if (onlyNonAdmin && new WindowsPrincipal(currentIdentity).IsInRole(WindowsBuiltInRole.Administrator)) { return pipeSecurity; } - pipeSecurity.Allow(currentIdentity.User, PipeAccessRights.ReadWrite|PipeAccessRights.CreateNewInstance); + + pipeSecurity.Allow(currentIdentity.User!, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance); } return pipeSecurity; } + [Browsable(false)] + [EditorBrowsable( EditorBrowsableState.Never)] public static bool PipeExists(string pipeName, int timeout = 1) { try @@ -161,26 +176,25 @@ private static async ValueTask WriteMessageCore(this Stream stream, RecyclableMe { using (recyclableStream) { - await recyclableStream.CopyToAsync(stream, 0, cancellationToken); + try + { + await recyclableStream.CopyToAsync(stream, 0, cancellationToken); + } + catch + { + throw; + } } } - internal static Task WriteBuffer(this Stream stream, byte[] buffer, CancellationToken cancellationToken) => + internal static Task WriteBuffer(this Stream stream, byte[] buffer, CancellationToken cancellationToken) => stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); } -public static class Validator +internal static class Validator { - public static void Validate(ServiceHostBuilder serviceHostBuilder) - { - foreach (var endpointSettings in serviceHostBuilder.Endpoints.Values) - { - endpointSettings.Validate(); - } - } - - public static void Validate(ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder - => Validate(typeof(TInterface), builder.CallbackContract); - public static void Validate(params Type[] contracts) + => Validate(contracts.AsEnumerable()); + + public static void Validate(IEnumerable contracts) { foreach (var contract in contracts.Where(c => c != null)) { @@ -267,26 +281,30 @@ private static void CheckDerivedStream(MethodInfo method, Type type) } } } -public readonly struct TimeoutHelper : IDisposable +internal readonly struct TimeoutHelper : IDisposable { private static readonly Action LinkedTokenCancelDelegate = static s => ((CancellationTokenSource)s).Cancel(); private readonly PooledCancellationTokenSource _timeoutCancellationSource; private readonly CancellationToken _cancellationToken; private readonly CancellationTokenRegistration _linkedRegistration; + public TimeoutHelper(TimeSpan timeout, CancellationToken token) { _timeoutCancellationSource = Rent(); _timeoutCancellationSource.CancelAfter(timeout); _cancellationToken = token; - _linkedRegistration = token.UnsafeRegister(LinkedTokenCancelDelegate, _timeoutCancellationSource); + _linkedRegistration = token.UnsafeRegister(LinkedTokenCancelDelegate!, _timeoutCancellationSource); } + + public static string ComputeTimeoutMessage(string operation) => $"{operation} timed out."; + public Exception CheckTimeout(Exception exception, string message) { if (_timeoutCancellationSource.IsCancellationRequested) { if (!_cancellationToken.IsCancellationRequested) { - return new TimeoutException(message + " timed out.", exception); + return new TimeoutException(ComputeTimeoutMessage(message), exception); } if (exception is not TaskCanceledException) { diff --git a/src/UiPath.CoreIpc/NestedStream.cs b/src/UiPath.CoreIpc/Helpers/NestedStream.cs similarity index 97% rename from src/UiPath.CoreIpc/NestedStream.cs rename to src/UiPath.CoreIpc/Helpers/NestedStream.cs index fdc40877..a726678d 100644 --- a/src/UiPath.CoreIpc/NestedStream.cs +++ b/src/UiPath.CoreIpc/Helpers/NestedStream.cs @@ -1,10 +1,10 @@ -namespace UiPath.CoreIpc; +namespace UiPath.Ipc; /// /// A stream that allows for reading from another stream up to a given number of bytes. /// https://github.com/AArnott/Nerdbank.Streams/blob/3303c541c29b979f61c86c3c2ed5c0e7372d7a55/src/Nerdbank.Streams/NestedStream.cs#L18 /// -public class NestedStream : Stream +internal class NestedStream : Stream { /// /// The stream to read from. @@ -35,7 +35,7 @@ public void Reset(long length) _length = length; } - public event EventHandler Disposed; + public event EventHandler? Disposed; /// public bool IsDisposed => _underlyingStream == null; /// @@ -148,7 +148,7 @@ protected override void Dispose(bool disposing) if (_remainingBytes != 0) { _underlyingStream?.Dispose(); - _underlyingStream = null; + _underlyingStream = null!; } Disposed?.Invoke(this, EventArgs.Empty); base.Dispose(disposing); diff --git a/src/UiPath.CoreIpc/Helpers/Result.cs b/src/UiPath.CoreIpc/Helpers/Result.cs new file mode 100644 index 00000000..7f47b3de --- /dev/null +++ b/src/UiPath.CoreIpc/Helpers/Result.cs @@ -0,0 +1,21 @@ +namespace UiPath.Ipc; + +internal readonly struct Result +{ + private readonly T _value; + private readonly Exception? _exception; + + public T Value => _exception is null ? _value : throw _exception; + + public Result(T value) + { + _value = value; + _exception = null; + } + + public Result(Exception exception) + { + _value = default!; + _exception = exception; + } +} diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs new file mode 100644 index 00000000..9a023c46 --- /dev/null +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -0,0 +1,144 @@ +namespace UiPath.Ipc; + +internal readonly record struct RouterConfig(IReadOnlyDictionary Endpoints) +{ + public static RouterConfig From(ContractCollection endpoints, Func transform) + { + ContractToSettingsMap nameToEndpoint = []; + + foreach (var endpoint in endpoints) + { + var newEndpoint = transform(endpoint); + foreach (var iface in endpoint.Service.Type.GetInterfaces().Prepend(endpoint.Service.Type)) + { + nameToEndpoint[iface.Name] = newEndpoint; + } + } + + return new(nameToEndpoint); + } +} + +internal readonly struct Router +{ + private readonly RouterConfig? _config; // nullable for the case when the constructor is bypassed + private readonly IServiceProvider? _serviceProvider; + + public Router(IpcServer ipcServer) + { + _config = ipcServer.CreateRouterConfig(ipcServer); + _serviceProvider = ipcServer.ServiceProvider; + } + + public Router(RouterConfig config, IServiceProvider? serviceProvider) + { + _config = config; + _serviceProvider = serviceProvider; + } + + public bool TryResolve(string endpoint, out Route route) + { + if (_config is not { } config) /// in case was allocated as default(Router), bypassing the constructor + { + throw new InvalidOperationException(); + } + + if (config.Endpoints.TryGetValue(endpoint, out var endpointSettings)) + { + route = Route.From(_serviceProvider, endpointSettings); + return true; + } + + route = default; + return false; + } +} + +internal abstract record ServiceFactory +{ + public required Type Type { get; init; } + + public abstract IDisposable? Get(out object service); + + public virtual ServiceFactory WithProvider(IServiceProvider? serviceProvider) => this; + + internal virtual object? MaybeGetInstance() => null; + internal virtual IServiceProvider? MaybeGetServiceProvider() => null; + + public sealed record Injected : ServiceFactory + { + public required IServiceProvider ServiceProvider { get; init; } + + internal override IServiceProvider? MaybeGetServiceProvider() => ServiceProvider; + + public override IDisposable? Get(out object service) + { + var scope = ServiceProvider.CreateScope(); + service = scope.ServiceProvider.GetRequiredService(Type); + return scope; + } + + public override ServiceFactory WithProvider(IServiceProvider? serviceProvider) + { + if (serviceProvider is null) + { + throw new InvalidOperationException(); + } + + return this with { ServiceProvider = serviceProvider }; + } + } + + public sealed record Instance : ServiceFactory + { + public required object ServiceInstance { get; init; } + + internal override object? MaybeGetInstance() => ServiceInstance; + + public override IDisposable? Get(out object service) + { + service = ServiceInstance; + return null; + } + } + + public sealed record Deferred : ServiceFactory + { + public override IDisposable? Get(out object service) + { + throw new NotSupportedException(); + } + + public override ServiceFactory WithProvider(IServiceProvider? serviceProvider) + { + if (serviceProvider is null) + { + throw new InvalidOperationException(); + } + + return new Injected() + { + Type = Type, + ServiceProvider = serviceProvider + }; + } + } +} + +internal readonly struct Route +{ + public static Route From(IServiceProvider? serviceProvider, ContractSettings endpointSettings) + => new Route() + { + Service = endpointSettings.Service.WithProvider(serviceProvider), + BeforeCall = endpointSettings.BeforeIncomingCall, + Scheduler = endpointSettings.Scheduler.OrDefault(), + LoggerFactory = serviceProvider.MaybeCreateServiceFactory(), + }; + + public required ServiceFactory Service { get; init; } + + public TaskScheduler Scheduler { get; init; } + public BeforeCallHandler? BeforeCall { get; init; } + public Func? LoggerFactory { get; init; } +} diff --git a/src/UiPath.CoreIpc/TaskCompletionPool.cs b/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs similarity index 86% rename from src/UiPath.CoreIpc/TaskCompletionPool.cs rename to src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs index 11f9ece9..21bdd710 100644 --- a/src/UiPath.CoreIpc/TaskCompletionPool.cs +++ b/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks.Sources; -namespace UiPath.CoreIpc; +namespace UiPath.Ipc; internal static class TaskCompletionPool { public static ManualResetValueTaskSource Rent() => ObjectPool.Rent(); @@ -17,7 +17,7 @@ public sealed class ManualResetValueTaskSource : IValueTaskSource, IValueTask public T GetResult(short token) => _core.GetResult(token); void IValueTaskSource.GetResult(short token) => _core.GetResult(token); public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token); - public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags); public void Return() { Reset(); diff --git a/src/UiPath.CoreIpc/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/IpcJsonSerializer.cs deleted file mode 100644 index 33a9faf5..00000000 --- a/src/UiPath.CoreIpc/IpcJsonSerializer.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Buffers; -using System.Globalization; -using System.Text; -namespace UiPath.CoreIpc; - -public interface ISerializer -{ - ValueTask DeserializeAsync(Stream json); - object Deserialize(object json, Type type); - void Serialize(object obj, Stream stream); - string Serialize(object obj); - object Deserialize(string json, Type type); -} -class IpcJsonSerializer : ISerializer, IArrayPool -{ - static readonly JsonSerializer ObjectArgsSerializer = new(){ DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, NullValueHandling = NullValueHandling.Ignore, - CheckAdditionalContent = true }; - static readonly JsonSerializer StringArgsSerializer = new(){ CheckAdditionalContent = true }; -#if !NET461 - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] -#endif - public async ValueTask DeserializeAsync(Stream json) - { - using var stream = IOHelpers.GetStream((int)json.Length); - await json.CopyToAsync(stream); - stream.Position = 0; - using var reader = CreateReader(new StreamReader(stream)); - return ObjectArgsSerializer.Deserialize(reader); - } - public object Deserialize(object json, Type type) => json switch - { - JToken token => token.ToObject(type, ObjectArgsSerializer), - { } => type.IsAssignableFrom(json.GetType()) ? json : new JValue(json).ToObject(type), - null => null, - }; - public void Serialize(object obj, Stream stream) => Serialize(obj, new StreamWriter(stream), ObjectArgsSerializer); - private void Serialize(object obj, TextWriter streamWriter, JsonSerializer serializer) - { - using var writer = new JsonTextWriter(streamWriter) { ArrayPool = this, CloseOutput = false }; - serializer.Serialize(writer, obj); - writer.Flush(); - } - public char[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); - public void Return(char[] array) => ArrayPool.Shared.Return(array); - public string Serialize(object obj) - { - var stringWriter = new StringWriter(new StringBuilder(capacity: 256), CultureInfo.InvariantCulture); - Serialize(obj, stringWriter, StringArgsSerializer); - return stringWriter.ToString(); - } - public object Deserialize(string json, Type type) - { - using var reader = CreateReader(new StringReader(json)); - return StringArgsSerializer.Deserialize(reader, type); - } - private JsonTextReader CreateReader(TextReader json) => new(json){ ArrayPool = this }; -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs b/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs new file mode 100644 index 00000000..e4523187 --- /dev/null +++ b/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs @@ -0,0 +1,72 @@ +namespace UiPath.Ipc; + +using static LoggingExtensions.Event; + +internal static partial class LoggingExtensions +{ + private const string ServiceClient = "ServiceClient"; + private const string Connection = "Connection"; + + private const int Jump = 1000; + private enum EventCategory + { + ServiceClient = Jump * 0, + Connection = Jump * 1 + } + + public enum Event + { + ServiceClient = EventCategory.ServiceClient, + ServiceClient_Calling = ServiceClient + 1, + ServiceClient_CalledSuccessfully = ServiceClient + 2, + ServiceClient_FailedToCall = ServiceClient + 3, + ServiceClient_Dispose = ServiceClient + 4, + + Connection = EventCategory.Connection, + Connection_ReceiveLoopFailed = Connection + 1, + Connection_ReceiveLoopEndedSuccessfully = Connection + 2, + + } + + [LoggerMessage( + EventId = (int)Event.ServiceClient_Calling, + EventName = nameof(Event.ServiceClient_Calling), + Level = LogLevel.Debug, + Message = $$"""{{ServiceClient}} calling {methodName} {requestId} {debugName}.""")] + public static partial void ServiceClient_Calling(this ILogger logger, string methodName, string requestId, string debugName); + + [LoggerMessage( + EventId = (int)Event.ServiceClient_CalledSuccessfully, + EventName = nameof(Event.ServiceClient_CalledSuccessfully), + Level = LogLevel.Debug, + Message = $$"""{{ServiceClient}} successfully called a remote method. MethodName={methodName}, RequestId={requestId}, DebugName={debugName}.""")] + public static partial void ServiceClient_CalledSuccessfully(this ILogger logger, string methodName, string requestId, string debugName); + + [LoggerMessage( + EventId = (int)Event.ServiceClient_FailedToCall, + EventName = nameof(Event.ServiceClient_FailedToCall), + Level = LogLevel.Debug, + Message = $$"""{{ServiceClient}} failed to call a remote method. MethodName={methodName}, RequestId={requestId}, DebugName={debugName}.""")] + public static partial void ServiceClient_FailedToCall(this ILogger logger, string methodName, string requestId, string debugName, Exception ex); + + [LoggerMessage( + EventId = (int)Event.ServiceClient_Dispose, + EventName = nameof(Event.ServiceClient_Dispose), + Level = LogLevel.Debug, + Message = $$"""{{ServiceClient}} disposed. DebugName={debugName}.""")] + public static partial void ServiceClient_Dispose(this ILogger logger, string debugName); + + [LoggerMessage( + EventId = (int)Event.Connection_ReceiveLoopFailed, + EventName = nameof(Event.Connection_ReceiveLoopFailed), + Level = LogLevel.Error, + Message = $$"""{{Connection}} receive loop failed. DebugName={debugName}.""")] + public static partial void Connection_ReceiveLoopFailed(this ILogger logger, string debugName, Exception ex); + + [LoggerMessage( + EventId = (int)Event.Connection_ReceiveLoopEndedSuccessfully, + EventName = nameof(Event.Connection_ReceiveLoopEndedSuccessfully), + Level = LogLevel.Debug, + Message = $$"""{{Connection}} receive loop ended successfully. DebugName={debugName}.""")] + public static partial void Connection_ReceiveLoopEndedSuccessfully(this ILogger logger, string debugName); +} diff --git a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs b/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs deleted file mode 100644 index 61584f39..00000000 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.IO.Pipes; -using System.Security.Principal; - -namespace UiPath.CoreIpc.NamedPipe; - -using ConnectionFactory = Func>; -using BeforeCallHandler = Func; - -interface INamedPipeKey : IConnectionKey -{ - string ServerName { get; } - string PipeName { get; } - bool AllowImpersonation { get; } -} - -class NamedPipeClient : ServiceClient, INamedPipeKey where TInterface : class -{ - public NamedPipeClient(string serverName, string pipeName, ISerializer serializer, TimeSpan requestTimeout, bool allowImpersonation, ILogger logger, ConnectionFactory connectionFactory, string sslServer, BeforeCallHandler beforeCall, bool objectParameters, EndpointSettings serviceEndpoint) - : base(serializer, requestTimeout, logger, connectionFactory, sslServer, beforeCall, objectParameters, serviceEndpoint) - { - ServerName = serverName; - PipeName = pipeName; - AllowImpersonation = allowImpersonation; - HashCode = (serverName, pipeName, allowImpersonation, sslServer).GetHashCode(); - } - public override string Name => base.Name ?? PipeName; - public string ServerName { get; } - public string PipeName { get; } - public bool AllowImpersonation { get; } - public override bool Equals(IConnectionKey other) => other == this || (other is INamedPipeKey otherClient && - otherClient.ServerName == ServerName && otherClient.PipeName == PipeName && otherClient.AllowImpersonation == AllowImpersonation && base.Equals(other)); - public override ClientConnection CreateClientConnection() => new NamedPipeClientConnection(this); - class NamedPipeClientConnection : ClientConnection - { - private NamedPipeClientStream _pipe; - public NamedPipeClientConnection(IConnectionKey connectionKey) : base(connectionKey) { } - public override bool Connected => _pipe?.IsConnected is true; - protected override void Dispose(bool disposing) - { - _pipe?.Dispose(); - base.Dispose(disposing); - } - public override async Task Connect(CancellationToken cancellationToken) - { - var key = (INamedPipeKey)ConnectionKey; - _pipe = new(key.ServerName, key.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous, key.AllowImpersonation ? TokenImpersonationLevel.Impersonation : TokenImpersonationLevel.Identification); - await _pipe.ConnectAsync(cancellationToken); - return _pipe; - } - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs b/src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs deleted file mode 100644 index 5e4b69d4..00000000 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace UiPath.CoreIpc.NamedPipe; - -public abstract class NamedPipeClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder -{ - private readonly string _pipeName; - private string _serverName = "."; - private bool _allowImpersonation; - - protected NamedPipeClientBuilderBase(string pipeName, Type callbackContract = null, IServiceProvider serviceProvider = null) : base(callbackContract, serviceProvider) => _pipeName = pipeName; - - public TDerived ServerName(string serverName) - { - _serverName = serverName; - return this as TDerived; - } - - /// - /// Don't set this if you can connect to less privileged processes. - /// Allow impersonation is false by default to prevent an escalation of privilege attack. - /// If a privileged process connects to a less privileged one and the proxy allows impersonation then the server could impersonate the client's identity. - /// - /// this - public TDerived AllowImpersonation() - { - _allowImpersonation = true; - return this as TDerived; - } - - protected override TInterface BuildCore(EndpointSettings serviceEndpoint) => - new NamedPipeClient(_serverName, _pipeName, _serializer, _requestTimeout, _allowImpersonation, _logger, _connectionFactory, _sslServer, _beforeCall, _objectParameters, serviceEndpoint).CreateProxy(); -} - -public class NamedPipeClientBuilder : NamedPipeClientBuilderBase, TInterface> where TInterface : class -{ - public NamedPipeClientBuilder(string pipeName) : base(pipeName){} -} - -public class NamedPipeClientBuilder : NamedPipeClientBuilderBase, TInterface> where TInterface : class where TCallbackInterface : class -{ - public NamedPipeClientBuilder(string pipeName, IServiceProvider serviceProvider) : base(pipeName, typeof(TCallbackInterface), serviceProvider) { } - - public NamedPipeClientBuilder CallbackInstance(TCallbackInterface singleton) - { - _callbackInstance = singleton; - return this; - } - - public NamedPipeClientBuilder TaskScheduler(TaskScheduler taskScheduler) - { - _taskScheduler = taskScheduler; - return this; - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs b/src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs deleted file mode 100644 index 5f408757..00000000 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.IO.Pipes; -using System.Security.Principal; - -namespace UiPath.CoreIpc.NamedPipe; - -public class NamedPipeSettings : ListenerSettings -{ - public NamedPipeSettings(string pipeName) : base(pipeName) { } - public Action AccessControl { get; set; } -} -class NamedPipeListener : Listener -{ - public NamedPipeListener(NamedPipeSettings settings) : base(settings) { } - protected override ServerConnection CreateServerConnection() => new NamedPipeServerConnection(this); - class NamedPipeServerConnection : ServerConnection - { - readonly NamedPipeServerStream _server; - public NamedPipeServerConnection(Listener listener) : base(listener) - { - _server = IOHelpers.NewNamedPipeServerStream(Settings.Name, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, PipeOptions.Asynchronous, GetPipeSecurity); - } - public override async Task AcceptClient(CancellationToken cancellationToken) - { - await _server.WaitForConnectionAsync(cancellationToken); - return _server; - } - public override void Impersonate(Action action) => _server.RunAsClient(()=>action()); - protected override void Dispose(bool disposing) - { - _server.Dispose(); - base.Dispose(disposing); - } - PipeSecurity GetPipeSecurity() - { - var setAccessControl = ((NamedPipeSettings)Settings).AccessControl; - if (setAccessControl == null) - { - return null; - } - var pipeSecurity = new PipeSecurity(); - FullControlFor(WellKnownSidType.BuiltinAdministratorsSid); - FullControlFor(WellKnownSidType.LocalSystemSid); - pipeSecurity.AllowCurrentUser(onlyNonAdmin: true); - setAccessControl(pipeSecurity); - return pipeSecurity; - void FullControlFor(WellKnownSidType sid) => pipeSecurity.Allow(sid, PipeAccessRights.FullControl); - } - } -} -public static class NamedPipeServiceExtensions -{ - public static ServiceHostBuilder UseNamedPipes(this ServiceHostBuilder builder, NamedPipeSettings settings) => - builder.AddListener(new NamedPipeListener(settings)); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..45ded093 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,35 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +// +// Summary: +// Allows capturing of the expressions passed to a method. +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + // + // Summary: + // Initializes a new instance of the System.Runtime.CompilerServices.CallerArgumentExpressionAttribute + // class. + // + // Parameters: + // parameterName: + // The name of the targeted parameter. + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + // + // Summary: + // Gets the target parameter name of the CallerArgumentExpression. + // + // Returns: + // The name of the targeted parameter of the CallerArgumentExpression. + public string ParameterName { get; } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/CancellationTokenExtensions.cs b/src/UiPath.CoreIpc/Polyfills/CancellationTokenExtensions.cs new file mode 100644 index 00000000..95b0cde0 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/CancellationTokenExtensions.cs @@ -0,0 +1,11 @@ +#if NET461 + +namespace System.Threading; + +internal static class CancellationTokenExtensions +{ + public static CancellationTokenRegistration UnsafeRegister(this CancellationToken token, Action callback, object? state) + => token.Register(callback, state); +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/CompilerFeatureRequiredAttribute.cs b/src/UiPath.CoreIpc/Polyfills/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 00000000..c40e1129 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,56 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +/// +/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: AttributeTargets.All, + AllowMultiple = true, + Inherited = false)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.compilerfeaturerequiredattribute")] +#if PolyPublic +public +#endif +sealed class CompilerFeatureRequiredAttribute : + Attribute +{ + /// + /// Initialize a new instance of + /// + /// The name of the required compiler feature. + public CompilerFeatureRequiredAttribute(string featureName) => + FeatureName = featureName; + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof(RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof(RequiredMembers); +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs b/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs new file mode 100644 index 00000000..dba5280f --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +#if NET461 + +namespace System.Linq; + +internal static class EnumerableExtensions +{ + public static IEnumerable Prepend(this IEnumerable enumerable, T element) + { + yield return element; + foreach (var item in enumerable) + { + yield return item; + } + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/IsExternalInit.cs b/src/UiPath.CoreIpc/Polyfills/IsExternalInit.cs new file mode 100644 index 00000000..dc521fbf --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/IsExternalInit.cs @@ -0,0 +1,19 @@ +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +/// +/// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal static class IsExternalInit +{ +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/MemberNotNullAttribute.cs b/src/UiPath.CoreIpc/Polyfills/MemberNotNullAttribute.cs new file mode 100644 index 00000000..bf9ad270 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/MemberNotNullAttribute.cs @@ -0,0 +1,71 @@ +// +#pragma warning disable + +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX + +namespace System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +/// +/// Specifies that the method or property will ensure that the listed field and property members have +/// non- values when returning with the specified return value condition. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Method | + Targets.Property, + Inherited = false, + AllowMultiple = true)] +#if PolyPublic +public +#endif +sealed class MemberNotNullWhenAttribute : + Attribute +{ + /// + /// Gets the return value condition. + /// + public bool ReturnValue { get; } + + /// + /// Gets field or property member names. + /// + public string[] Members { get; } + + /// + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// + /// The field or property member that is promised to be not-. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// + /// Initializes the attribute with the specified return value condition and list + /// of field and property members. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs b/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs new file mode 100644 index 00000000..b49daaab --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs @@ -0,0 +1,38 @@ +// +#pragma warning disable + +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX + +namespace System.Diagnostics.CodeAnalysis; + +// +// Summary: +// Specifies that the method or property will ensure that the listed field and property +// members have values that aren't null. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +internal sealed class MemberNotNullAttribute : Attribute +{ + // + // Summary: + // Initializes the attribute with a field or property member. + // + // Parameters: + // member: + // The field or property member that is promised to be non-null. + public MemberNotNullAttribute(string member) : this([member]) { } + // + // Summary: + // Initializes the attribute with the list of field and property members. + // + // Parameters: + // members: + // The list of field and property members that are promised to be non-null. + public MemberNotNullAttribute(params string[] members) => Members = members; + + // + // Summary: + // Gets field or property member names. + public string[] Members { get; } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/NotNullIfNotNullAttribute.cs b/src/UiPath.CoreIpc/Polyfills/NotNullIfNotNullAttribute.cs new file mode 100644 index 00000000..50afab43 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/NotNullIfNotNullAttribute.cs @@ -0,0 +1,41 @@ +// +#pragma warning disable + +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X + +namespace System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Parameter | + Targets.Property | + Targets.ReturnValue, + AllowMultiple = true)] +#if PolyPublic +public +#endif +sealed class NotNullIfNotNullAttribute : + Attribute +{ + /// + /// Gets the associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public string ParameterName { get; } + + /// + /// Initializes the attribute with the associated parameter name. + /// + /// + /// The associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public NotNullIfNotNullAttribute(string parameterName) => + ParameterName = parameterName; +} +#endif diff --git a/src/UiPath.CoreIpc/Polyfills/NotNullWhenAttribute.cs b/src/UiPath.CoreIpc/Polyfills/NotNullWhenAttribute.cs new file mode 100644 index 00000000..be043ad9 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/NotNullWhenAttribute.cs @@ -0,0 +1,38 @@ +// +#pragma warning disable + +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that when a method returns , +/// the parameter will not be even if the corresponding type allows it. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Parameter)] +#if PolyPublic +public +#endif +sealed class NotNullWhenAttribute : + Attribute +{ + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public NotNullWhenAttribute(bool returnValue) => + ReturnValue = returnValue; +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/RequiredMemberAttribute.cs b/src/UiPath.CoreIpc/Polyfills/RequiredMemberAttribute.cs new file mode 100644 index 00000000..8564df41 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/RequiredMemberAttribute.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +using Targets = AttributeTargets; + +/// +/// Specifies that a type has required members or that a member is required. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Class | + Targets.Struct | + Targets.Field | + Targets.Property, + Inherited = false)] +#if PolyPublic +public +#endif +sealed class RequiredMemberAttribute : + Attribute; +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/SetsRequiredMembersAttribute.cs b/src/UiPath.CoreIpc/Polyfills/SetsRequiredMembersAttribute.cs new file mode 100644 index 00000000..f3db6439 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/SetsRequiredMembersAttribute.cs @@ -0,0 +1,20 @@ +// +#pragma warning disable + +#if !NET7_0_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that this constructor sets all required members for the current type, and callers +/// do not need to set any required members themselves. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Constructor)] +#if PolyPublic +public +#endif +sealed class SetsRequiredMembersAttribute : + Attribute; +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/System_Index.cs b/src/UiPath.CoreIpc/Polyfills/System_Index.cs new file mode 100644 index 00000000..177b155c --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/System_Index.cs @@ -0,0 +1,151 @@ +// +#pragma warning disable + +#if !NET5_0_OR_GREATER + +namespace System; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal readonly struct Index : IEquatable +{ + readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (fromEnd) + { + _value = ~value; + } + else + { + _value = value; + } + } + + // The following private constructors mainly created for perf reason to avoid the checks + Index(int value) => + _value = value; + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new IndexOutOfRangeException(nameof(value)); + } + + return new(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object value) => value is Index index && _value == index._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + { + return ToStringFromEnd(); + } + + return ((uint)Value).ToString(); + } + + string ToStringFromEnd() => + '^' + Value.ToString(); +} +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/TcpClientExtensions.cs b/src/UiPath.CoreIpc/Polyfills/TcpClientExtensions.cs new file mode 100644 index 00000000..10b4a728 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/TcpClientExtensions.cs @@ -0,0 +1,14 @@ +#if NET461 + +namespace System.Net.Sockets; + +internal static class TcpClientExtensions +{ + public static async Task ConnectAsync(this TcpClient tcpClient, IPAddress address, int port, CancellationToken cancellationToken) + { + using var token = cancellationToken.Register(state => (state as TcpClient)!.Dispose(), tcpClient); + await tcpClient.ConnectAsync(address, port); + } +} + +#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/ContractSettings.cs b/src/UiPath.CoreIpc/Server/ContractSettings.cs new file mode 100644 index 00000000..45ba6eda --- /dev/null +++ b/src/UiPath.CoreIpc/Server/ContractSettings.cs @@ -0,0 +1,44 @@ +namespace UiPath.Ipc; + +using System; + +public sealed class ContractSettings +{ + public TaskScheduler? Scheduler { get; set; } + public BeforeCallHandler? BeforeIncomingCall { get; set; } + internal ServiceFactory Service { get; } + + internal Type ContractType => Service.Type; + internal object? ServiceInstance => Service.MaybeGetInstance(); + internal IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider(); + + public ContractSettings(Type contractType, object? serviceInstance = null) : this( + serviceInstance is not null + ? new ServiceFactory.Instance() + { + Type = contractType ?? throw new ArgumentNullException(nameof(contractType)), + ServiceInstance = serviceInstance + } + : new ServiceFactory.Deferred() + { + Type = contractType ?? throw new ArgumentNullException(nameof(contractType)), + }) + { } + + public ContractSettings(Type contractType, IServiceProvider serviceProvider) : this( + new ServiceFactory.Injected() + { + Type = contractType ?? throw new ArgumentNullException(nameof(contractType)), + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)) + }) + { } + + private ContractSettings(ServiceFactory service) => Service = service; + + internal ContractSettings(ContractSettings other) + { + Scheduler = other.Scheduler; + BeforeIncomingCall = other.BeforeIncomingCall; + Service = other.Service; + } +} diff --git a/src/UiPath.CoreIpc/Server/IClient.cs b/src/UiPath.CoreIpc/Server/IClient.cs new file mode 100644 index 00000000..1fa7c645 --- /dev/null +++ b/src/UiPath.CoreIpc/Server/IClient.cs @@ -0,0 +1,7 @@ +namespace UiPath.Ipc; + +public interface IClient +{ + TCallbackInterface GetCallback() where TCallbackInterface : class; + void Impersonate(Action action); +} diff --git a/src/UiPath.CoreIpc/Server/Listener.cs b/src/UiPath.CoreIpc/Server/Listener.cs deleted file mode 100644 index a9c10adf..00000000 --- a/src/UiPath.CoreIpc/Server/Listener.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Security.Cryptography.X509Certificates; - -namespace UiPath.CoreIpc; - -public class ListenerSettings -{ - public ListenerSettings(string name) => Name = name; - public byte ConcurrentAccepts { get; set; } = 5; - public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; - public X509Certificate Certificate { get; set; } - public string Name { get; } - public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; - internal IServiceProvider ServiceProvider { get; set; } - internal IDictionary Endpoints { get; set; } -} -abstract class Listener : IDisposable -{ - protected Listener(ListenerSettings settings) - { - Settings = settings; - MaxMessageSize = settings.MaxReceivedMessageSizeInMegabytes * 1024 * 1024; - } - public string Name => Settings.Name; - public ILogger Logger { get; private set; } - public IServiceProvider ServiceProvider => Settings.ServiceProvider; - public ListenerSettings Settings { get; } - public int MaxMessageSize { get; } - public Task Listen(CancellationToken token) - { - Logger = ServiceProvider.GetRequiredService().CreateLogger(GetType()); - if (LogEnabled) - { - Log($"Starting listener {Name}..."); - } - return Task.WhenAll(Enumerable.Range(1, Settings.ConcurrentAccepts).Select(async _ => - { - while (!token.IsCancellationRequested) - { - await AcceptConnection(token); - } - })); - } - protected abstract ServerConnection CreateServerConnection(); - async Task AcceptConnection(CancellationToken token) - { - var serverConnection = CreateServerConnection(); - try - { - var network = await serverConnection.AcceptClient(token); - serverConnection.Listen(network, token).LogException(Logger, Name); - } - catch (Exception ex) - { - serverConnection.Dispose(); - if (!token.IsCancellationRequested) - { - Logger.LogException(ex, Settings.Name); - } - } - } - protected virtual void Dispose(bool disposing) - { - if (!disposing) - { - return; - } - Settings.Certificate?.Dispose(); - } - public void Dispose() - { - if (LogEnabled) - { - Log($"Stopping listener {Name}..."); - } - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - public void Log(string message) => Logger.LogInformation(message); - public bool LogEnabled => Logger.Enabled(); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index 5e6d5b4d..8fe18368 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -1,20 +1,39 @@ using System.Linq.Expressions; -namespace UiPath.CoreIpc; + +namespace UiPath.Ipc; + using GetTaskResultFunc = Func; -using MethodExecutor = Func; +using MethodExecutor = Func; using static Expression; using static CancellationTokenSourcePool; -class Server + +internal class Server { - private static readonly MethodInfo GetResultMethod = typeof(Server).GetStaticMethod(nameof(GetTaskResultImpl)); - private static readonly ConcurrentDictionary<(Type,string), Method> Methods = new(); + static Server() + { + var prototype = GetTaskResultImpl; + GetResultMethod = prototype.Method.GetGenericMethodDefinition(); + } + + private static readonly MethodInfo GetResultMethod; + private static readonly ConcurrentDictionary Methods = new(); private static readonly ConcurrentDictionary GetTaskResultByType = new(); + + private readonly Router _router; private readonly Connection _connection; - private readonly IClient _client; + private readonly IClient? _client; private readonly ConcurrentDictionary _requests = new(); - public Server(ListenerSettings settings, Connection connection, IClient client = null) + + private readonly TimeSpan _requestTimeout; + + private ILogger? Logger => _connection.Logger; + private bool LogEnabled => _connection.LogEnabled; + public string DebugName => _connection.DebugName; + + public Server(Router router, TimeSpan requestTimeout, Connection connection, IClient? client = null) { - Settings = settings; + _router = router; + _requestTimeout = requestTimeout; _connection = connection; _client = client; connection.RequestReceived += OnRequestReceived; @@ -23,7 +42,7 @@ public Server(ListenerSettings settings, Connection connection, IClient client = { if (LogEnabled) { - Log($"Server {Name} closed."); + Log($"Server {DebugName} closed."); } foreach (var requestId in _requests.Keys) { @@ -33,12 +52,13 @@ public Server(ListenerSettings settings, Connection connection, IClient client = } catch (Exception ex) { - Logger.LogException(ex, $"{Name}"); + Logger.OrDefault().LogException(ex, $"{DebugName}"); } } }; } - void CancelRequest(string requestId) + + private void CancelRequest(string requestId) { if (_requests.TryRemove(requestId, out var cancellation)) { @@ -46,44 +66,41 @@ void CancelRequest(string requestId) cancellation.Return(); } } + #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] #endif - async ValueTask OnRequestReceived(Request request) + private async ValueTask OnRequestReceived(Request request) { try { if (LogEnabled) { - Log($"{Name} received request {request}"); - } - if (!Endpoints.TryGetValue(request.Endpoint, out var endpoint)) - { - await OnError(request, new ArgumentOutOfRangeException(nameof(request.Endpoint), $"{Name} cannot find endpoint {request.Endpoint}")); - return; + Log($"{DebugName} received request {request}"); } - var method = GetMethod(endpoint.Contract, request.MethodName); - if (request.HasObjectParameters && !method.ReturnType.IsGenericType) + if (!_router.TryResolve(request.Endpoint, out var route)) { - await HandleRequest(method, endpoint, request, default); + await OnError(request, new EndpointNotFoundException(nameof(request.Endpoint), DebugName, request.Endpoint)); return; } - Response response = null; + var method = GetMethod(route.Service.Type, request.MethodName); + Response? response = null; var requestCancellation = Rent(); _requests[request.Id] = requestCancellation; - var timeout = request.GetTimeout(Settings.RequestTimeout); + var timeout = request.GetTimeout(_requestTimeout); var timeoutHelper = new TimeoutHelper(timeout, requestCancellation.Token); try { var token = timeoutHelper.Token; - response = await HandleRequest(method, endpoint, request, token); + response = await HandleRequest(method, route, request, token); + if (LogEnabled) { - Log($"{Name} sending response for {request}"); + Log($"{DebugName} sending response for {request}"); } await SendResponse(response, token); } - catch (Exception ex) when(response == null) + catch (Exception ex) when (response is null) { await OnError(request, timeoutHelper.CheckTimeout(ex, request.MethodName)); } @@ -94,7 +111,7 @@ async ValueTask OnRequestReceived(Request request) } catch (Exception ex) { - Logger.LogException(ex, $"{Name} {request}"); + Logger.LogException(ex, $"{DebugName} {request}"); } if (_requests.TryRemove(request.Id, out var cancellation)) { @@ -103,81 +120,85 @@ async ValueTask OnRequestReceived(Request request) } ValueTask OnError(Request request, Exception ex) { - Logger.LogException(ex, $"{Name} {request}"); + Logger.LogException(ex, $"{DebugName} {request}"); return SendResponse(Response.Fail(request, ex), default); } + #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] #endif - async ValueTask HandleRequest(Method method, EndpointSettings endpoint, Request request, CancellationToken cancellationToken) + private async ValueTask HandleRequest(Method method, Route route, Request request, CancellationToken cancellationToken) { - var objectParameters = request.HasObjectParameters; - var contract = endpoint.Contract; var arguments = GetArguments(); - var beforeCall = endpoint.BeforeCall; - if (beforeCall != null) + + object service; + using (route.Service.Get(out service)) { - await beforeCall(new(default, method.MethodInfo, arguments), cancellationToken); - } - IServiceScope scope = null; - var service = endpoint.ServiceInstance; - try - { - if (service == null) - { - scope = ServiceProvider.CreateScope(); - service = scope.ServiceProvider.GetRequiredService(contract); - } return await InvokeMethod(); } - finally - { - scope?.Dispose(); - } #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] #endif async ValueTask InvokeMethod() { var returnTaskType = method.ReturnType; - var scheduler = endpoint.Scheduler; - Debug.Assert(scheduler != null); + var scheduler = route.Scheduler; var defaultScheduler = scheduler == TaskScheduler.Default; + + Debug.Assert(scheduler != null); + if (returnTaskType.IsGenericType) { - var methodResult = defaultScheduler ? MethodCall() : await RunOnScheduler(); - await methodResult; - var returnValue = GetTaskResult(returnTaskType, methodResult); - if (returnValue is Stream downloadStream) + var result = await ScheduleMethodCall(); + return result switch { - return Response.Success(request, downloadStream); - } - return objectParameters ? new Response(request.Id, ObjectData: returnValue) : Response.Success(request, Serializer.Serialize(returnValue)); + Stream downloadStream => Response.Success(request, downloadStream), + var x => Response.Success(request, IpcJsonSerializer.Instance.Serialize(x)) + }; } - else + + ScheduleMethodCall().LogException(Logger, method.MethodInfo); + return Response.Success(request, ""); + + Task ScheduleMethodCall() => defaultScheduler ? MethodCall() : RunOnScheduler(); + async Task MethodCall() { - (defaultScheduler ? MethodCall() : RunOnScheduler().Unwrap()).LogException(Logger, method.MethodInfo); - return objectParameters ? null : Response.Success(request, ""); + await (route.BeforeCall?.Invoke( + new CallInfo(newConnection: false, method.MethodInfo, arguments), + cancellationToken) ?? Task.CompletedTask); + + Task invocationTask = null!; + + invocationTask = method.Invoke(service, arguments, cancellationToken); + await invocationTask; + + if (!returnTaskType.IsGenericType) + { + return null; + } + + return GetTaskResult(returnTaskType, invocationTask); } - Task MethodCall() => method.Invoke(service, arguments, cancellationToken); - Task RunOnScheduler() => Task.Factory.StartNew(MethodCall, cancellationToken, TaskCreationOptions.DenyChildAttach, scheduler); + + Task RunOnScheduler() => Task.Factory.StartNew(MethodCall, cancellationToken, TaskCreationOptions.DenyChildAttach, scheduler).Unwrap(); } - object[] GetArguments() + object?[] GetArguments() { var parameters = method.Parameters; var allParametersLength = parameters.Length; - var requestParametersLength = objectParameters ? request.ObjectParameters.Length : request.Parameters.Length; + var requestParametersLength = request.Parameters.Length; if (requestParametersLength > allParametersLength) { throw new ArgumentException("Too many parameters for " + method.MethodInfo); } - var allArguments = objectParameters && requestParametersLength == allParametersLength ? request.ObjectParameters : new object[allParametersLength]; + var allArguments = new object?[allParametersLength]; Deserialize(); SetOptionalArguments(); + return allArguments; void Deserialize() { - object argument; + object? argument; for (int index = 0; index < requestParametersLength; index++) { var parameterType = parameters[index].ParameterType; @@ -191,25 +212,21 @@ void Deserialize() } else { - argument = objectParameters ? - Serializer.Deserialize(request.ObjectParameters[index], parameterType) : - Serializer.Deserialize(request.Parameters[index], parameterType); + argument = IpcJsonSerializer.Instance.Deserialize(request.Parameters[index], parameterType); argument = CheckMessage(argument, parameterType); } allArguments[index] = argument; } } - object CheckMessage(object argument, Type parameterType) + object? CheckMessage(object? argument, Type parameterType) { - if (parameterType == typeof(Message) && argument == null) + if (parameterType == typeof(Message) && argument is null) { argument = new Message(); } if (argument is Message message) { - message.CallbackContract = endpoint.CallbackContract; - message.Client = _client; - message.ObjectParameters = objectParameters; + message.Client = _client!; } return argument; } @@ -222,46 +239,57 @@ void SetOptionalArguments() } } } - private void Log(string message) => _connection.Log(message); - private ILogger Logger => _connection.Logger; - private bool LogEnabled => Logger.Enabled(); - private ListenerSettings Settings { get; } - public IServiceProvider ServiceProvider => Settings.ServiceProvider; - public ISerializer Serializer => _connection.Serializer; - public string Name => _connection.Name; - public IDictionary Endpoints => Settings.Endpoints; - ValueTask SendResponse(Response response, CancellationToken responseCancellation) => _connection.Send(response, responseCancellation); - static object GetTaskResultImpl(Task task) => ((Task)task).Result; - static object GetTaskResult(Type taskType, Task task) => - GetTaskResultByType.GetOrAdd(taskType.GenericTypeArguments[0], - resultType => GetResultMethod.MakeGenericDelegate(resultType))(task); - static Method GetMethod(Type contract, string methodName) => Methods.GetOrAdd((contract, methodName), - ((Type contract,string methodName) key) => new(key.contract.GetInterfaceMethod(key.methodName))); - readonly struct Method + + private void Log(string message) => Logger.OrDefault().LogInformation(message); + + private ValueTask SendResponse(Response response, CancellationToken responseCancellation) => _connection.Send(response, responseCancellation); + + private static object? GetTaskResultImpl(Task task) => (task as Task)!.Result; + + private static object GetTaskResult(Type taskType, Task task) + => GetTaskResultByType.GetOrAdd( + taskType.GenericTypeArguments[0], + GetResultMethod.MakeGenericDelegate)(task); + + private static Method GetMethod(Type contract, string methodName) + => Methods.GetOrAdd(new(contract, methodName), Method.FromKey); + + private readonly record struct MethodKey(Type Contract, string MethodName); + + private readonly struct Method { - static readonly ParameterExpression TargetParameter = Parameter(typeof(object), "target"); - static readonly ParameterExpression TokenParameter = Parameter(typeof(CancellationToken), "cancellationToken"); - static readonly ParameterExpression ParametersParameter = Parameter(typeof(object[]), "parameters"); - readonly MethodExecutor _executor; + public static Method FromKey(MethodKey key) + { + var methodInfo = key.Contract.GetInterfaceMethod(key.MethodName); + return new(methodInfo); + } + + private static readonly ParameterExpression TargetParameter = Parameter(typeof(object), "target"); + private static readonly ParameterExpression TokenParameter = Parameter(typeof(CancellationToken), "cancellationToken"); + private static readonly ParameterExpression ParametersParameter = Parameter(typeof(object[]), "parameters"); + + private readonly MethodExecutor _executor; public readonly MethodInfo MethodInfo; public readonly ParameterInfo[] Parameters; - public readonly object[] Defaults; + public readonly object?[] Defaults; + public Type ReturnType => MethodInfo.ReturnType; - public Method(MethodInfo method) + + private Method(MethodInfo method) { // https://github.com/dotnet/aspnetcore/blob/3f620310883092905ed6f13d784c908b5f4a9d7e/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs#L156 var parameters = method.GetParameters(); var parametersLength = parameters.Length; var callParameters = new Expression[parametersLength]; - var defaults = new object[parametersLength]; + var defaults = new object?[parametersLength]; for (int index = 0; index < parametersLength; index++) { var parameter = parameters[index]; defaults[index] = parameter.GetDefaultValue(); - callParameters[index] = parameter.ParameterType == typeof(CancellationToken) ? TokenParameter : + callParameters[index] = parameter.ParameterType == typeof(CancellationToken) ? TokenParameter : Convert(ArrayIndex(ParametersParameter, Constant(index, typeof(int))), parameter.ParameterType); } - var instanceCast = Convert(TargetParameter, method.DeclaringType); + var instanceCast = Convert(TargetParameter, method.DeclaringType!); var methodCall = Call(instanceCast, method, callParameters); var lambda = Lambda(methodCall, TargetParameter, ParametersParameter, TokenParameter); _executor = lambda.Compile(); @@ -269,7 +297,9 @@ public Method(MethodInfo method) Parameters = parameters; Defaults = defaults; } - public Task Invoke(object service, object[] arguments, CancellationToken cancellationToken) => _executor.Invoke(service, arguments, cancellationToken); - public override string ToString() => MethodInfo.ToString(); + + public Task Invoke(object service, object?[] arguments, CancellationToken cancellationToken) => _executor.Invoke(service, arguments, cancellationToken); + + public override string ToString() => MethodInfo.ToString()!; } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index 43f11a6a..64db7415 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -1,85 +1,80 @@ -using System.Net.Security; -namespace UiPath.CoreIpc; +using System.IO.Pipes; -public interface IClient -{ - TCallbackInterface GetCallback(Type callbackContract, bool objectParameters) where TCallbackInterface : class; - void Impersonate(Action action); -} -abstract class ServerConnection : IClient, IDisposable +namespace UiPath.Ipc; + +internal sealed class ServerConnection : IClient, IDisposable, IClientConfig { - private readonly ConcurrentDictionary _callbacks = new(); - protected readonly Listener _listener; - private Connection _connection; - private Task _connectionAsTask; - private Server _server; - protected ServerConnection(Listener listener) => _listener = listener; - public ILogger Logger => _listener.Logger; - public ListenerSettings Settings => _listener.Settings; - public abstract Task AcceptClient(CancellationToken cancellationToken); - public virtual void Impersonate(Action action) => action(); - TCallbackInterface IClient.GetCallback(Type callbackContract, bool objectParameters) where TCallbackInterface : class + public static void CreateAndListen(IpcServer server, Stream network, CancellationToken ct) { - if (callbackContract == null) + _ = Task.Run(async () => { - throw new InvalidOperationException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's not configured."); - } - return (TCallbackInterface)_callbacks.GetOrAdd(callbackContract, CreateCallback); - TCallbackInterface CreateCallback(Type callbackContract) - { - if (!typeof(TCallbackInterface).IsAssignableFrom(callbackContract)) - { - throw new ArgumentException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's {callbackContract}."); - } - if (_listener.LogEnabled) - { - _listener.Log($"Create callback {callbackContract} {_listener.Name}"); - } - _connectionAsTask ??= Task.FromResult(_connection); - var serviceClient = new ServiceClient(_connection.Serializer, Settings.RequestTimeout, Logger, (_, _) => _connectionAsTask) - { - ObjectParameters = objectParameters - }; - return serviceClient.CreateProxy(); - } + _ = new ServerConnection(server, await server.Transport.MaybeAuthenticate(network), ct); + }); } - public async Task Listen(Stream network, CancellationToken cancellationToken) + + private readonly string _debugName; + private readonly ILogger? _logger; + private readonly ConcurrentDictionary _callbacks = new(); + private readonly IpcServer _ipcServer; + + private readonly Stream _network; + private readonly Connection _connection; + private readonly Server _server; + + private readonly Task _listening; + + private ServerConnection(IpcServer server, Stream network, CancellationToken ct) + { + _ipcServer = server; + + _debugName = $"{nameof(ServerConnection)} {RuntimeHelpers.GetHashCode(this)}"; + _logger = server.CreateLogger(_debugName); + + _network = network; + + _connection = new Connection(network, _debugName, _logger, maxMessageSize: _ipcServer.Transport.MaxMessageSize); + _server = new Server(new Router(_ipcServer), _ipcServer.RequestTimeout, _connection, client: this); + + _listening = Listen(ct); + } + + private async Task Listen(CancellationToken ct) { - var stream = await AuthenticateAsServer(); - var serializer = Settings.ServiceProvider.GetRequiredService(); - _connection = new(stream, serializer, Logger, _listener.Name, _listener.MaxMessageSize); - _server = new(Settings, _connection, this); // close the connection when the service host closes - using (cancellationToken.UnsafeRegister(state => ((Connection)state).Dispose(), _connection)) + using (ct.UnsafeRegister(_ => _connection.Dispose(), state: null)) { await _connection.Listen(); } - return; - async Task AuthenticateAsServer() + } + + void IDisposable.Dispose() => _network.Dispose(); + + TCallbackInterface IClient.GetCallback() + { + return (TCallbackInterface)_callbacks.GetOrAdd(typeof(TCallbackInterface), CreateCallback); + + TCallbackInterface CreateCallback(Type callbackContract) { - var certificate = Settings.Certificate; - if (certificate == null) - { - return network; - } - var sslStream = new SslStream(network); - try - { - await sslStream.AuthenticateAsServerAsync(certificate); - } - catch - { - sslStream.Dispose(); - throw; - } - Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); - return sslStream; + _logger?.LogInformation($"Create callback {callbackContract}."); + return new ServiceClientForCallback(_connection, config: this).GetProxy(); } } - protected virtual void Dispose(bool disposing){} - public void Dispose() + void IClient.Impersonate(Action action) { - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (_connection.Network is not NamedPipeServerStream pipeStream) + { + action(); + return; + } + + pipeStream.RunAsClient(() => action()); } -} \ No newline at end of file + + #region IServiceClientConfig + TimeSpan IClientConfig.RequestTimeout => _ipcServer.RequestTimeout; + BeforeConnectHandler? IClientConfig.BeforeConnect => null; + BeforeCallHandler? IClientConfig.BeforeOutgoingCall => null; + ILogger? IClientConfig.Logger => _logger; + string IClientConfig.GetComputedDebugName() => _debugName; + #endregion +} diff --git a/src/UiPath.CoreIpc/Server/ServiceHost.cs b/src/UiPath.CoreIpc/Server/ServiceHost.cs deleted file mode 100644 index 13a834b1..00000000 --- a/src/UiPath.CoreIpc/Server/ServiceHost.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace UiPath.CoreIpc; -public sealed class ServiceHost : IDisposable -{ - private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly IDictionary _endpoints; - private readonly IReadOnlyCollection _listeners; - internal ServiceHost(IEnumerable listeners, IDictionary endpoints) - { - _endpoints = endpoints.ToReadOnlyDictionary(); - _listeners = listeners.ToArray(); - } - public void Dispose() - { - if(_cancellationTokenSource.IsCancellationRequested) - { - return; - } - foreach (var listener in _listeners) - { - listener.Dispose(); - } - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.AssertDisposed(); - } - public void Run() => RunAsync().Wait(); - public Task RunAsync(TaskScheduler taskScheduler = null) - { - foreach (var endpoint in _endpoints.Values) - { - endpoint.Scheduler = taskScheduler; - } - return Task.Run(() => Task.WhenAll(_listeners.Select(listener => listener.Listen(_cancellationTokenSource.Token)))); - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs b/src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs deleted file mode 100644 index e5c3474f..00000000 --- a/src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace UiPath.CoreIpc; - -using BeforeCallHandler = Func; -public class ServiceHostBuilder -{ - private readonly List _listeners = new(); - public ServiceHostBuilder(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider; - internal IServiceProvider ServiceProvider { get; } - internal Dictionary Endpoints { get; } = new(); - public ServiceHostBuilder AddEndpoint(EndpointSettings settings) - { - settings.ServiceProvider = ServiceProvider; - Endpoints.Add(settings.Name, settings); - return this; - } - internal ServiceHostBuilder AddListener(Listener listener) - { - listener.Settings.ServiceProvider = ServiceProvider; - listener.Settings.Endpoints = Endpoints; - _listeners.Add(listener); - return this; - } - public ServiceHost Build() => new(_listeners, Endpoints); -} -public static class ServiceHostBuilderExtensions -{ - public static ServiceHostBuilder AddEndpoints(this ServiceHostBuilder serviceHostBuilder, IEnumerable endpoints) - { - foreach (var endpoint in endpoints) - { - serviceHostBuilder.AddEndpoint(endpoint); - } - return serviceHostBuilder; - } - public static ServiceHostBuilder AddEndpoint(this ServiceHostBuilder serviceHostBuilder, TContract serviceInstance = null) where TContract : class => - serviceHostBuilder.AddEndpoint(new EndpointSettings(serviceInstance)); - public static ServiceHostBuilder AddEndpoint(this ServiceHostBuilder serviceHostBuilder, TContract serviceInstance = null) where TContract : class where TCallbackContract : class => - serviceHostBuilder.AddEndpoint(new EndpointSettings(serviceInstance)); -} -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddIpc(this IServiceCollection services) - { - services.AddSingleton(); - return services; - } -} -public class EndpointSettings -{ - private TaskScheduler _scheduler; - public EndpointSettings(Type contract, object serviceInstance = null, Type callbackContract = null) - { - Contract = contract ?? throw new ArgumentNullException(nameof(contract)); - Name = contract.Name; - ServiceInstance = serviceInstance; - CallbackContract = callbackContract; - } - internal string Name { get; } - internal TaskScheduler Scheduler { get => _scheduler; set => _scheduler = value ?? TaskScheduler.Default; } - internal object ServiceInstance { get; } - internal Type Contract { get; } - internal Type CallbackContract { get; } - internal IServiceProvider ServiceProvider { get; set; } - public BeforeCallHandler BeforeCall { get; set; } - public void Validate() => Validator.Validate(Contract, CallbackContract); -} -public class EndpointSettings : EndpointSettings where TContract : class -{ - public EndpointSettings(TContract serviceInstance = null, Type callbackContract = null) : base(typeof(TContract), serviceInstance, callbackContract) { } -} -public class EndpointSettings : EndpointSettings where TContract : class where TCallbackContract : class -{ - public EndpointSettings(TContract serviceInstance = null) : base(serviceInstance, typeof(TCallbackContract)) { } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Tcp/TcpClient.cs b/src/UiPath.CoreIpc/Tcp/TcpClient.cs deleted file mode 100644 index 50386575..00000000 --- a/src/UiPath.CoreIpc/Tcp/TcpClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace UiPath.CoreIpc.Tcp; - -using ConnectionFactory = Func>; -using BeforeCallHandler = Func; -interface ITcpKey : IConnectionKey -{ - IPEndPoint EndPoint { get; } -} -class TcpClient : ServiceClient, ITcpKey where TInterface : class -{ - public TcpClient(IPEndPoint endPoint, ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, string sslServer, BeforeCallHandler beforeCall, bool objectParameters, EndpointSettings serviceEndpoint) : base(serializer, requestTimeout, logger, connectionFactory, sslServer, beforeCall, objectParameters, serviceEndpoint) - { - EndPoint = endPoint; - HashCode = (EndPoint, sslServer).GetHashCode(); - } - public override string Name => base.Name ?? EndPoint.ToString(); - public IPEndPoint EndPoint { get; } - public override bool Equals(IConnectionKey other) => other == this || (other is ITcpKey otherClient && EndPoint.Equals(otherClient.EndPoint) && - base.Equals(other)); - public override ClientConnection CreateClientConnection() => new TcpClientConnection(this); - class TcpClientConnection : ClientConnection - { - private TcpClient _tcpClient; - public TcpClientConnection(IConnectionKey connectionKey) : base(connectionKey) {} - public override bool Connected => _tcpClient?.Client?.Connected is true; - protected override void Dispose(bool disposing) - { - _tcpClient?.Dispose(); - base.Dispose(disposing); - } - public override async Task Connect(CancellationToken cancellationToken) - { - _tcpClient = new(); - var endPoint = ((ITcpKey)ConnectionKey).EndPoint; - await _tcpClient.ConnectAsync(endPoint.Address, endPoint.Port, cancellationToken); - return _tcpClient.GetStream(); - } - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs b/src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs deleted file mode 100644 index a0a05b71..00000000 --- a/src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Net; - -namespace UiPath.CoreIpc.Tcp; - -public abstract class TcpClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder -{ - private readonly IPEndPoint _endPoint; - - protected TcpClientBuilderBase(IPEndPoint endPoint, Type callbackContract = null, IServiceProvider serviceProvider = null) : base(callbackContract, serviceProvider) => - _endPoint = endPoint; - - protected override TInterface BuildCore(EndpointSettings serviceEndpoint) => - new TcpClient(_endPoint, _serializer, _requestTimeout, _logger, _connectionFactory, _sslServer, _beforeCall, _objectParameters, serviceEndpoint).CreateProxy(); -} - -public class TcpClientBuilder : TcpClientBuilderBase, TInterface> where TInterface : class -{ - public TcpClientBuilder(IPEndPoint endPoint) : base(endPoint){} -} - -public class TcpClientBuilder : TcpClientBuilderBase, TInterface> where TInterface : class where TCallbackInterface : class -{ - public TcpClientBuilder(IPEndPoint endPoint, IServiceProvider serviceProvider) : base(endPoint, typeof(TCallbackInterface), serviceProvider) { } - - public TcpClientBuilder CallbackInstance(TCallbackInterface singleton) - { - _callbackInstance = singleton; - return this; - } - - public TcpClientBuilder TaskScheduler(TaskScheduler taskScheduler) - { - _taskScheduler = taskScheduler; - return this; - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Tcp/TcpListener.cs b/src/UiPath.CoreIpc/Tcp/TcpListener.cs deleted file mode 100644 index b2019e3c..00000000 --- a/src/UiPath.CoreIpc/Tcp/TcpListener.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Net; -namespace UiPath.CoreIpc.Tcp; - -public class TcpSettings : ListenerSettings -{ - public TcpSettings(IPEndPoint endPoint) : base(endPoint.ToString()) - { - EndPoint = endPoint; - } - public IPEndPoint EndPoint { get; } -} -class TcpListener : Listener -{ - readonly System.Net.Sockets.TcpListener _tcpServer; - public TcpListener(ListenerSettings settings) : base(settings) - { - _tcpServer = new(Settings.EndPoint); - _tcpServer.Start(backlog: Settings.ConcurrentAccepts); - } - public new TcpSettings Settings => (TcpSettings)base.Settings; - protected override ServerConnection CreateServerConnection() => new TcpServerConnection(this); - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _tcpServer.Stop(); - } - Task AcceptClient(CancellationToken cancellationToken) => _tcpServer.AcceptTcpClientAsync(); - class TcpServerConnection : ServerConnection - { - System.Net.Sockets.TcpClient _tcpClient; - public TcpServerConnection(Listener listener) : base(listener){} - public override async Task AcceptClient(CancellationToken cancellationToken) - { - _tcpClient = await ((TcpListener)_listener).AcceptClient(cancellationToken); - return _tcpClient.GetStream(); - } - protected override void Dispose(bool disposing) - { - _tcpClient?.Dispose(); - base.Dispose(disposing); - } - } -} -public static class TcpServiceExtensions -{ - public static ServiceHostBuilder UseTcp(this ServiceHostBuilder builder, TcpSettings settings) => builder.AddListener(new TcpListener(settings)); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs new file mode 100644 index 00000000..0ccfc08e --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs @@ -0,0 +1,51 @@ +using System.IO.Pipes; +using System.Security.Principal; + +namespace UiPath.Ipc.Transport.NamedPipe; + +public sealed record NamedPipeClientTransport : ClientTransport +{ + public required string PipeName { get; init; } + public string ServerName { get; init; } = "."; + public bool AllowImpersonation { get; init; } + + public override string ToString() => $"ClientPipe={PipeName}"; + + internal override IClientState CreateState() => new NamedPipeClientState(); + + internal override void Validate() + { + if (PipeName is null or "") + { + throw new InvalidOperationException($"{nameof(PipeName)} is required."); + } + if (ServerName is null or "") + { + throw new InvalidOperationException($"{nameof(ServerName)} is required."); + } + } +} + +internal sealed class NamedPipeClientState : IClientState +{ + private NamedPipeClientStream? _pipe; + + public Stream? Network => _pipe; + public bool IsConnected() => _pipe?.IsConnected is true; + + public async ValueTask Connect(IpcClient client, CancellationToken ct) + { + var transport = client.Transport as NamedPipeClientTransport ?? throw new InvalidOperationException(); + + _pipe = new NamedPipeClientStream( + transport.ServerName, + transport.PipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous, + transport.AllowImpersonation ? TokenImpersonationLevel.Impersonation : TokenImpersonationLevel.Identification); + + await _pipe.ConnectAsync(ct); + } + + public void Dispose() => _pipe?.Dispose(); +} diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs new file mode 100644 index 00000000..2319ae4c --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs @@ -0,0 +1,88 @@ +using Newtonsoft.Json; +using System.IO.Pipes; +using System.Security.Principal; + +namespace UiPath.Ipc.Transport.NamedPipe; + +public sealed class NamedPipeServerTransport : ServerTransport +{ + public required string PipeName { get; init; } + public string ServerName { get; init; } = "."; + [JsonIgnore] + public AccessControlDelegate? AccessControl { get; init; } + + internal override IServerState CreateServerState() + => new ServerState { Transport = this }; + + internal override IEnumerable ValidateCore() + { + yield return IsNotNull(PipeName); + yield return IsNotNull(ServerName); + } + + public override string ToString() => $"ServerPipe={PipeName}"; + + private sealed class ServerState : IServerState + { + public required NamedPipeServerTransport Transport { get; init; } + + IServerConnectionSlot IServerState.CreateConnectionSlot() => ServerConnectionState.Create(serverState: this); + + ValueTask IAsyncDisposable.DisposeAsync() => default; + } + + private sealed class ServerConnectionState : IServerConnectionSlot + { + public static ServerConnectionState Create(ServerState serverState) + { + return new() + { + Stream = CreateStream() + }; + + NamedPipeServerStream CreateStream() + => IOHelpers.NewNamedPipeServerStream( + serverState.Transport.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + GetPipeSecurity); + + PipeSecurity? GetPipeSecurity() + { + if (serverState.Transport.AccessControl is not { } setAccessControl) + { + return null; + } + + var pipeSecurity = new PipeSecurity(); + FullControlFor(WellKnownSidType.BuiltinAdministratorsSid); + FullControlFor(WellKnownSidType.LocalSystemSid); + pipeSecurity.AllowCurrentUser(onlyNonAdmin: true); + setAccessControl(pipeSecurity); + return pipeSecurity; + + void FullControlFor(WellKnownSidType sid) => pipeSecurity.Allow(sid, PipeAccessRights.FullControl); + } + } + + public required NamedPipeServerStream Stream { get; init; } + + ValueTask IAsyncDisposable.DisposeAsync() + { +#if NET461 + Stream.Dispose(); + return default; +#else + return Stream.DisposeAsync(); +#endif + } + + async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + { + await Stream.WaitForConnectionAsync(ct); + return Stream; + } + } +} diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs new file mode 100644 index 00000000..f82dc7f0 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs @@ -0,0 +1,56 @@ +using System.Net; + +namespace UiPath.Ipc.Transport.Tcp; + +public sealed record TcpClientTransport : ClientTransport +{ + public required IPEndPoint EndPoint { get; init; } + + public override string ToString() => $"TcpClient={EndPoint}"; + + internal override IClientState CreateState() => new TcpClientState(); + + internal override void Validate() + { + if (EndPoint is null) + { + throw new InvalidOperationException($"{nameof(EndPoint)} is required."); + } + } +} + +internal sealed class TcpClientState : IClientState +{ + private System.Net.Sockets.TcpClient? _tcpClient; + + public Stream? Network { get; private set; } + + public bool IsConnected() + { + return _tcpClient?.Client?.Connected is true; + } + + public async ValueTask Connect(IpcClient client, CancellationToken ct) + { + var transport = client.Transport as TcpClientTransport ?? throw new InvalidOperationException(); + + _tcpClient = new System.Net.Sockets.TcpClient(); +#if NET461 + using var ctreg = ct.Register(_tcpClient.Dispose); + try + { + await _tcpClient.ConnectAsync(transport.EndPoint.Address, transport.EndPoint.Port); + } + catch (ObjectDisposedException) + { + _tcpClient = null; + throw new OperationCanceledException(ct); + } +#else + await _tcpClient.ConnectAsync(transport.EndPoint.Address, transport.EndPoint.Port, ct); +#endif + Network = _tcpClient.GetStream(); + } + + public void Dispose() => _tcpClient?.Dispose(); +} diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs new file mode 100644 index 00000000..33c74f3d --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs @@ -0,0 +1,56 @@ +using System.Net; +using System.Net.Sockets; + +namespace UiPath.Ipc.Transport.Tcp; + +public sealed class TcpServerTransport : ServerTransport +{ + public required IPEndPoint EndPoint { get; init; } + + internal override IServerState CreateServerState() + { + var listener = new TcpListener(EndPoint); + listener.Start(backlog: ConcurrentAccepts); + return new ServerState() { TcpListener = listener }; + } + + internal override IEnumerable ValidateCore() + { + yield return IsNotNull(EndPoint); + } + + public override string ToString() => $"TcpServer={EndPoint}"; + + private sealed class ServerState : IServerState + { + public required TcpListener TcpListener { get; init; } + + ValueTask IAsyncDisposable.DisposeAsync() + { + TcpListener.Stop(); + return default; + } + + IServerConnectionSlot IServerState.CreateConnectionSlot() + => new ServerConnectionState { ServerState = this }; + } + + private sealed class ServerConnectionState : IServerConnectionSlot + { + public required ServerState ServerState { get; init; } + + async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + { + TcpClient tcpClient; +#if NET461 + using var ctreg = ct.Register(ServerState.TcpListener.Stop); + tcpClient = await ServerState.TcpListener.AcceptTcpClientAsync(); +#else + tcpClient = await ServerState.TcpListener.AcceptTcpClientAsync(ct); +#endif + return tcpClient.GetStream(); + } + + ValueTask IAsyncDisposable.DisposeAsync() => default; + } +} diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs new file mode 100644 index 00000000..6aa333f1 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs @@ -0,0 +1,39 @@ +using System.Net.WebSockets; + +namespace UiPath.Ipc.Transport.WebSocket; + +public sealed record WebSocketClientTransport : ClientTransport +{ + public required Uri Uri { get; init; } + public override string ToString() => $"WebSocketClient={Uri}"; + + internal override IClientState CreateState() => new WebSocketClientState(); + + internal override void Validate() + { + if (Uri is null) + { + throw new InvalidOperationException($"{nameof(Uri)} is required."); + } + } +} + +internal sealed class WebSocketClientState : IClientState +{ + private ClientWebSocket? _clientWebSocket; + + public Stream? Network { get; private set; } + + public bool IsConnected() => _clientWebSocket?.State is WebSocketState.Open; + + public async ValueTask Connect(IpcClient client, CancellationToken ct) + { + var transport = client.Transport as WebSocketClientTransport ?? throw new InvalidOperationException(); + + _clientWebSocket = new(); + await _clientWebSocket.ConnectAsync(transport.Uri, ct); + Network = new WebSocketStream(_clientWebSocket); + } + + public void Dispose() => _clientWebSocket?.Dispose(); +} diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs new file mode 100644 index 00000000..e81536c1 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs @@ -0,0 +1,31 @@ +namespace UiPath.Ipc.Transport.WebSocket; + +public sealed class WebSocketServerTransport : ServerTransport +{ + public required Accept Accept { get; init; } + + internal override IServerState CreateServerState() => new State { Transport = this }; + + internal override IEnumerable ValidateCore() + { + yield return IsNotNull(Accept); + } + + public override string ToString() => nameof(WebSocketServerTransport); + + private sealed class State : IServerState, IServerConnectionSlot + { + public required WebSocketServerTransport Transport { get; init; } + + public async ValueTask AwaitConnection(CancellationToken ct) + { + var webSocket = await Transport.Accept(ct); + return new WebSocketStream(webSocket); + } + + public IServerConnectionSlot CreateConnectionSlot() => this; + + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } +} diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs similarity index 95% rename from src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs rename to src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs index c64b4816..91ef319e 100644 --- a/src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs @@ -1,10 +1,14 @@ using System.Net.WebSockets; -namespace UiPath.CoreIpc.WebSockets; + +namespace UiPath.Ipc.Transport.WebSocket; + +using WebSocket = System.Net.WebSockets.WebSocket; + /// /// Exposes a as a . /// https://github.com/AArnott/Nerdbank.Streams/blob/main/src/Nerdbank.Streams/WebSocketStream.cs /// -public class WebSocketStream : Stream +internal class WebSocketStream : Stream { /// /// The socket wrapped by this stream. diff --git a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index b0111764..fa462395 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -1,8 +1,10 @@  net6.0;net461;net6.0-windows - UiPath.CoreIpc + UiPath.Ipc + UiPath.Ipc true + true UiPath 2.5.1 https://github.com/UiPath/CoreIpc/ @@ -14,25 +16,38 @@ true snupkg CA1416 - latest + preview true + enable + true + - - <_Parameter1>UiPath.CoreIpc.Tests - - + + + + + + + + + + + + + + diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs b/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs deleted file mode 100644 index f0774988..00000000 --- a/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Net.WebSockets; -namespace UiPath.CoreIpc.WebSockets; -using ConnectionFactory = Func>; -using BeforeCallHandler = Func; -interface IWebSocketsKey : IConnectionKey -{ - Uri Uri { get; } -} -class WebSocketClient : ServiceClient, IWebSocketsKey where TInterface : class -{ - public WebSocketClient(Uri uri, ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, string sslServer, BeforeCallHandler beforeCall, bool objectParameters, EndpointSettings serviceEndpoint) : base(serializer, requestTimeout, logger, connectionFactory, sslServer, beforeCall, objectParameters, serviceEndpoint) - { - Uri = uri; - HashCode = (uri, sslServer).GetHashCode(); - } - public override string Name => base.Name ?? Uri.ToString(); - public Uri Uri { get; } - public override bool Equals(IConnectionKey other) => other == this || (other is IWebSocketsKey otherClient && Uri.Equals(otherClient.Uri) && base.Equals(other)); - public override ClientConnection CreateClientConnection() => new WebSocketClientConnection(this); - class WebSocketClientConnection : ClientConnection - { - ClientWebSocket _clientWebSocket; - public WebSocketClientConnection(IConnectionKey connectionKey) : base(connectionKey) {} - public override bool Connected => _clientWebSocket?.State == WebSocketState.Open; - protected override void Dispose(bool disposing) - { - _clientWebSocket?.Dispose(); - base.Dispose(disposing); - } - public override async Task Connect(CancellationToken cancellationToken) - { - _clientWebSocket = new(); - var uri = ((IWebSocketsKey)ConnectionKey).Uri; - await _clientWebSocket.ConnectAsync(uri, cancellationToken); - return new WebSocketStream(_clientWebSocket); - } - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs b/src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs deleted file mode 100644 index 893a8b7a..00000000 --- a/src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace UiPath.CoreIpc.WebSockets; -public abstract class WebSocketClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder -{ - private readonly Uri _uri; - protected WebSocketClientBuilderBase(Uri uri, Type callbackContract = null, IServiceProvider serviceProvider = null) : base(callbackContract, serviceProvider) => - _uri = uri; - protected override TInterface BuildCore(EndpointSettings serviceEndpoint) => - new WebSocketClient(_uri, _serializer, _requestTimeout, _logger, _connectionFactory, _sslServer, _beforeCall, _objectParameters, serviceEndpoint).CreateProxy(); -} -public class WebSocketClientBuilder : WebSocketClientBuilderBase, TInterface> where TInterface : class -{ - public WebSocketClientBuilder(Uri uri) : base(uri){} -} -public class WebSocketClientBuilder : WebSocketClientBuilderBase, TInterface> where TInterface : class where TCallbackInterface : class -{ - public WebSocketClientBuilder(Uri uri, IServiceProvider serviceProvider) : base(uri, typeof(TCallbackInterface), serviceProvider) { } - public WebSocketClientBuilder CallbackInstance(TCallbackInterface singleton) - { - _callbackInstance = singleton; - return this; - } - public WebSocketClientBuilder TaskScheduler(TaskScheduler taskScheduler) - { - _taskScheduler = taskScheduler; - return this; - } -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs b/src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs deleted file mode 100644 index 20b95a06..00000000 --- a/src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net.WebSockets; -namespace UiPath.CoreIpc.WebSockets; -using Accept = Func>; -public class WebSocketSettings : ListenerSettings -{ - public WebSocketSettings(Accept accept) : base("") => Accept = accept; - public Accept Accept { get; } -} -class WebSocketListener : Listener -{ - public WebSocketListener(ListenerSettings settings) : base(settings){} - protected override ServerConnection CreateServerConnection() => new WebSocketConnection(this); - class WebSocketConnection : ServerConnection - { - public WebSocketConnection(Listener listener) : base(listener){} - public override async Task AcceptClient(CancellationToken cancellationToken) => - new WebSocketStream(await ((WebSocketSettings)_listener.Settings).Accept(cancellationToken)); - } -} -public static class WebSocketServiceExtensions -{ - public static ServiceHostBuilder UseWebSockets(this ServiceHostBuilder builder, WebSocketSettings settings) => - builder.AddListener(new WebSocketListener(settings)); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Dtos.cs b/src/UiPath.CoreIpc/Wire/Dtos.cs similarity index 57% rename from src/UiPath.CoreIpc/Dtos.cs rename to src/UiPath.CoreIpc/Wire/Dtos.cs index cbf278ba..bbb3e5da 100644 --- a/src/UiPath.CoreIpc/Dtos.cs +++ b/src/UiPath.CoreIpc/Wire/Dtos.cs @@ -1,53 +1,67 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; using Newtonsoft.Json; -namespace UiPath.CoreIpc; + +namespace UiPath.Ipc; + public class Message { - internal bool ObjectParameters { get; set; } - internal Type CallbackContract { get; set; } [JsonIgnore] - public IClient Client { get; set; } + public IClient Client { get; set; } = null!; [JsonIgnore] public TimeSpan RequestTimeout { get; set; } - public TCallbackInterface GetCallback() where TCallbackInterface : class => - Client.GetCallback(CallbackContract, ObjectParameters); - public void ImpersonateClient(Action action) => Client.Impersonate(action); } public class Message : Message { public Message(TPayload payload) => Payload = payload; public TPayload Payload { get; } } -record Request(string Endpoint, string Id, string MethodName, string[] Parameters, object[] ObjectParameters, double TimeoutInSeconds) +internal record Request(string Endpoint, string Id, string MethodName, string[] Parameters, double TimeoutInSeconds) { - internal Stream UploadStream { get; set; } + [JsonIgnore] + public Stream? UploadStream { get; set; } + public override string ToString() => $"{Endpoint} {MethodName} {Id}."; - internal bool HasObjectParameters => ObjectParameters is not null; - internal TimeSpan GetTimeout(TimeSpan defaultTimeout) => TimeoutInSeconds == 0 ? defaultTimeout : TimeSpan.FromSeconds(TimeoutInSeconds); + + public TimeSpan GetTimeout(TimeSpan defaultTimeout) => TimeoutInSeconds == 0 ? defaultTimeout : TimeSpan.FromSeconds(TimeoutInSeconds); } record CancellationRequest(string RequestId); -record Response(string RequestId, string Data = null, object ObjectData = null, Error Error = null) + +internal record Response(string RequestId, string? Data = null, Error? Error = null) { - internal Stream DownloadStream { get; set; } + [JsonIgnore] + public Stream? DownloadStream { get; set; } + public static Response Fail(Request request, Exception ex) => new(request.Id, Error: ex.ToError()); public static Response Success(Request request, string data) => new(request.Id, data); public static Response Success(Request request, Stream downloadStream) => new(request.Id) { DownloadStream = downloadStream }; - public TResult Deserialize(ISerializer serializer, bool objectParameters) - { + public TResult Deserialize() + { if (Error != null) { throw new RemoteException(Error); } - return (TResult)(DownloadStream ?? (objectParameters ? - serializer.Deserialize(ObjectData, typeof(TResult)) : serializer.Deserialize(Data ?? "", typeof(TResult)))); + + return (TResult)(DownloadStream ?? IpcJsonSerializer.Instance.Deserialize(Data ?? "", typeof(TResult)))!; } } -[Serializable] -public record Error(string Message, string StackTrace, string Type, Error InnerError) + +public record Error(string Message, string StackTrace, string Type, Error? InnerError) { + [return: NotNullIfNotNull("exception")] + public static Error? FromException(Exception? exception) + => exception is null + ? null + : new( + Message: exception.Message, + StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!, + Type: GetExceptionType(exception), + InnerError: FromException(exception.InnerException)); public override string ToString() => new RemoteException(this).ToString(); + + private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!; } -[Serializable] + public class RemoteException : Exception { public RemoteException(Error error) : base(error.Message, error.InnerError == null ? null : new RemoteException(error.InnerError)) @@ -57,7 +71,7 @@ public class RemoteException : Exception } public string Type { get; } public override string StackTrace { get; } - public new RemoteException InnerException => (RemoteException)base.InnerException; + public new RemoteException? InnerException => base.InnerException as RemoteException; public override string ToString() { var result = new StringBuilder(); @@ -81,4 +95,4 @@ private void GatherInnerExceptions(StringBuilder result) } public bool Is() where TException : Exception => Type == typeof(TException).FullName; } -enum MessageType : byte { Request, Response, CancellationRequest, UploadRequest, DownloadResponse } \ No newline at end of file +internal enum MessageType : byte { Request, Response, CancellationRequest, UploadRequest, DownloadResponse } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs new file mode 100644 index 00000000..238c06ea --- /dev/null +++ b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs @@ -0,0 +1,16 @@ +namespace UiPath.Ipc; + +public sealed class EndpointNotFoundException : ArgumentException +{ + public string ServerDebugName { get; } + public string EndpointName { get; } + + internal EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) + : base(FormatMessage(serverDebugName, endpointName), paramName) + { + ServerDebugName = serverDebugName; + EndpointName = endpointName; + } + + internal static string FormatMessage(string serverDebugName, string endpointName) => $"Endpoint not found. Server was \"{serverDebugName}\". Endpoint was \"{endpointName}\"."; +} diff --git a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs new file mode 100644 index 00000000..32f29140 --- /dev/null +++ b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; +using System.Buffers; +using System.Globalization; +using System.Text; + +namespace UiPath.Ipc; + +internal class IpcJsonSerializer : IArrayPool +{ + public static readonly IpcJsonSerializer Instance = new(); + + static readonly JsonSerializer StringArgsSerializer = new() { CheckAdditionalContent = true }; + +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] +#endif + public async ValueTask DeserializeAsync(Stream json, ILogger? logger) + { + using var stream = IOHelpers.GetStream((int)json.Length); + await json.CopyToAsync(stream); + stream.Position = 0; + using var reader = CreateReader(new StreamReader(stream)); + return StringArgsSerializer.Deserialize(reader); + } + public void Serialize(object? obj, Stream stream) => Serialize(obj, new StreamWriter(stream), StringArgsSerializer); + private void Serialize(object? obj, TextWriter streamWriter, JsonSerializer serializer) + { + using var writer = new JsonTextWriter(streamWriter) { ArrayPool = this, CloseOutput = false }; + serializer.Serialize(writer, obj); + writer.Flush(); + } + char[] IArrayPool.Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); + void IArrayPool.Return(char[]? array) + { + if (array is null) + { + return; + } + + ArrayPool.Shared.Return(array); + } + + public string Serialize(object? obj) + { + var stringWriter = new StringWriter(new StringBuilder(capacity: 256), CultureInfo.InvariantCulture); + Serialize(obj, stringWriter, StringArgsSerializer); + return stringWriter.ToString(); + } + public object? Deserialize(string json, Type type) + { + using var reader = CreateReader(new StringReader(json)); + return StringArgsSerializer.Deserialize(reader, type); + } + private JsonTextReader CreateReader(TextReader json) => new(json) { ArrayPool = this }; +} diff --git a/src/UiPath.CoreIpc/report/UiPath.Ipc.net461.received.txt b/src/UiPath.CoreIpc/report/UiPath.Ipc.net461.received.txt new file mode 100644 index 00000000..4283b1b7 --- /dev/null +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net461.received.txt @@ -0,0 +1,196 @@ +[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/UiPath/coreipc.git")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Playground")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.BackCompat")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.Extensions.Abstractions")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.Ipc.Tests")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETFramework,Version=v4.6.1", FrameworkDisplayName=".NET Framework 4.6.1")] +namespace UiPath.Ipc +{ + public readonly struct CallInfo + { + public CallInfo(bool newConnection, System.Reflection.MethodInfo method, object?[] arguments) { } + public object?[] Arguments { get; } + public System.Reflection.MethodInfo Method { get; } + public bool NewConnection { get; } + } + public abstract class ClientTransport : System.IEquatable { } + public class ContractCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + public ContractCollection() { } + public void Add(System.Type contractType) { } + public void Add(UiPath.Ipc.ContractSettings endpointSettings) { } + public void Add(System.Type contractType, object? instance) { } + public System.Collections.Generic.IEnumerator GetEnumerator() { } + } + public sealed class ContractSettings + { + public ContractSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } + public ContractSettings(System.Type contractType, object? serviceInstance = null) { } + public System.Func? BeforeIncomingCall { get; set; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + } + public sealed class EndpointNotFoundException : System.ArgumentException + { + public string EndpointName { get; } + public string ServerDebugName { get; } + } + public class Error : System.IEquatable + { + public Error(string Message, string StackTrace, string Type, UiPath.Ipc.Error? InnerError) { } + public UiPath.Ipc.Error? InnerError { get; init; } + public string Message { get; init; } + public string StackTrace { get; init; } + public string Type { get; init; } + public override string ToString() { } + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("exception")] + public static UiPath.Ipc.Error? FromException(System.Exception? exception) { } + } + public interface IClient + { + TCallbackInterface GetCallback() + where TCallbackInterface : class; + void Impersonate(System.Action action); + } + public static class IOHelpers + { + public static System.IO.Pipes.PipeSecurity Allow(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.IdentityReference sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity Allow(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity AllowCurrentUser(this System.IO.Pipes.PipeSecurity pipeSecurity, bool onlyNonAdmin = false) { } + public static System.IO.Pipes.PipeSecurity Deny(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.IdentityReference sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity Deny(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity LocalOnly(this System.IO.Pipes.PipeSecurity pipeSecurity) { } + [System.ComponentModel.Browsable(false)] + public static bool PipeExists(string pipeName, int timeout = 1) { } + } + public abstract class IpcBase + { + protected IpcBase() { } + public System.TimeSpan RequestTimeout { get; set; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + public System.IServiceProvider? ServiceProvider { get; set; } + } + public sealed class IpcClient : UiPath.Ipc.IpcBase + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public IpcClient() { } + public System.Func? BeforeConnect { get; set; } + public System.Func? BeforeOutgoingCall { get; set; } + public UiPath.Ipc.ContractCollection? Callbacks { get; set; } + public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } + public UiPath.Ipc.ClientTransport Transport { get; init; } + public TProxy GetProxy() + where TProxy : class { } + } + public class IpcProxy : System.Reflection.DispatchProxy, System.IDisposable + { + public IpcProxy() { } + public System.IO.Stream? Network { get; } + public event System.EventHandler ConnectionClosed; + public System.Threading.Tasks.ValueTask CloseConnection() { } + public void Dispose() { } + protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) { } + } + public sealed class IpcServer : UiPath.Ipc.IpcBase, System.IAsyncDisposable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public IpcServer() { } + public UiPath.Ipc.ContractCollection Endpoints { get; init; } + public UiPath.Ipc.ServerTransport Transport { get; init; } + public System.Threading.Tasks.ValueTask DisposeAsync() { } + public void Start() { } + public System.Threading.Tasks.Task WaitForStart() { } + } + public class Message + { + public Message() { } + [Newtonsoft.Json.JsonIgnore] + public UiPath.Ipc.IClient Client { get; set; } + [Newtonsoft.Json.JsonIgnore] + public System.TimeSpan RequestTimeout { get; set; } + } + public class Message : UiPath.Ipc.Message + { + public Message(TPayload payload) { } + public TPayload Payload { get; } + } + public class RemoteException : System.Exception + { + public RemoteException(UiPath.Ipc.Error error) { } + public UiPath.Ipc.RemoteException? InnerException { get; } + public override string StackTrace { get; } + public string Type { get; } + public bool Is() + where TException : System.Exception { } + public override string ToString() { } + } + public abstract class ServerTransport + { + public int ConcurrentAccepts { get; set; } + public byte MaxReceivedMessageSizeInMegabytes { get; set; } + } +} +namespace UiPath.Ipc.Transport.NamedPipe +{ + public sealed class NamedPipeClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeClientTransport() { } + public bool AllowImpersonation { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override string ToString() { } + } + public sealed class NamedPipeServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeServerTransport() { } + [Newtonsoft.Json.JsonIgnore] + public System.Action? AccessControl { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override string ToString() { } + } +} +namespace UiPath.Ipc.Transport.Tcp +{ + public sealed class TcpClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpClientTransport() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override string ToString() { } + } + public sealed class TcpServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpServerTransport() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override string ToString() { } + } +} +namespace UiPath.Ipc.Transport.WebSocket +{ + public sealed class WebSocketClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketClientTransport() { } + public System.Uri Uri { get; init; } + public override string ToString() { } + } + public sealed class WebSocketServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketServerTransport() { } + public System.Func> Accept { get; init; } + public override string ToString() { } + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt new file mode 100644 index 00000000..fbe0a167 --- /dev/null +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -0,0 +1,198 @@ +[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/UiPath/coreipc.git")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Playground")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.BackCompat")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.Extensions.Abstractions")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.CoreIpc.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.Ipc.Tests")] +[assembly: System.Runtime.Versioning.SupportedOSPlatform("Windows7.0")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] +[assembly: System.Runtime.Versioning.TargetPlatform("Windows7.0")] +namespace UiPath.Ipc +{ + public readonly struct CallInfo + { + public CallInfo(bool newConnection, System.Reflection.MethodInfo method, object?[] arguments) { } + public object?[] Arguments { get; } + public System.Reflection.MethodInfo Method { get; } + public bool NewConnection { get; } + } + public abstract class ClientTransport : System.IEquatable { } + public class ContractCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + public ContractCollection() { } + public void Add(System.Type contractType) { } + public void Add(UiPath.Ipc.ContractSettings endpointSettings) { } + public void Add(System.Type contractType, object? instance) { } + public System.Collections.Generic.IEnumerator GetEnumerator() { } + } + public sealed class ContractSettings + { + public ContractSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } + public ContractSettings(System.Type contractType, object? serviceInstance = null) { } + public System.Func? BeforeIncomingCall { get; set; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + } + public sealed class EndpointNotFoundException : System.ArgumentException + { + public string EndpointName { get; } + public string ServerDebugName { get; } + } + public class Error : System.IEquatable + { + public Error(string Message, string StackTrace, string Type, UiPath.Ipc.Error? InnerError) { } + public UiPath.Ipc.Error? InnerError { get; init; } + public string Message { get; init; } + public string StackTrace { get; init; } + public string Type { get; init; } + public override string ToString() { } + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("exception")] + public static UiPath.Ipc.Error? FromException(System.Exception? exception) { } + } + public interface IClient + { + TCallbackInterface GetCallback() + where TCallbackInterface : class; + void Impersonate(System.Action action); + } + public static class IOHelpers + { + public static System.IO.Pipes.PipeSecurity Allow(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.IdentityReference sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity Allow(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity AllowCurrentUser(this System.IO.Pipes.PipeSecurity pipeSecurity, bool onlyNonAdmin = false) { } + public static System.IO.Pipes.PipeSecurity Deny(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.IdentityReference sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity Deny(this System.IO.Pipes.PipeSecurity pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) { } + public static System.IO.Pipes.PipeSecurity LocalOnly(this System.IO.Pipes.PipeSecurity pipeSecurity) { } + [System.ComponentModel.Browsable(false)] + public static bool PipeExists(string pipeName, int timeout = 1) { } + } + public abstract class IpcBase + { + protected IpcBase() { } + public System.TimeSpan RequestTimeout { get; set; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + public System.IServiceProvider? ServiceProvider { get; set; } + } + public sealed class IpcClient : UiPath.Ipc.IpcBase + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public IpcClient() { } + public System.Func? BeforeConnect { get; set; } + public System.Func? BeforeOutgoingCall { get; set; } + public UiPath.Ipc.ContractCollection? Callbacks { get; set; } + public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } + public UiPath.Ipc.ClientTransport Transport { get; init; } + public TProxy GetProxy() + where TProxy : class { } + } + public class IpcProxy : System.Reflection.DispatchProxy, System.IDisposable + { + public IpcProxy() { } + public System.IO.Stream? Network { get; } + public event System.EventHandler ConnectionClosed; + public System.Threading.Tasks.ValueTask CloseConnection() { } + public void Dispose() { } + protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) { } + } + public sealed class IpcServer : UiPath.Ipc.IpcBase, System.IAsyncDisposable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public IpcServer() { } + public UiPath.Ipc.ContractCollection Endpoints { get; init; } + public UiPath.Ipc.ServerTransport Transport { get; init; } + public System.Threading.Tasks.ValueTask DisposeAsync() { } + public void Start() { } + public System.Threading.Tasks.Task WaitForStart() { } + } + public class Message + { + public Message() { } + [Newtonsoft.Json.JsonIgnore] + public UiPath.Ipc.IClient Client { get; set; } + [Newtonsoft.Json.JsonIgnore] + public System.TimeSpan RequestTimeout { get; set; } + } + public class Message : UiPath.Ipc.Message + { + public Message(TPayload payload) { } + public TPayload Payload { get; } + } + public class RemoteException : System.Exception + { + public RemoteException(UiPath.Ipc.Error error) { } + public UiPath.Ipc.RemoteException? InnerException { get; } + public override string StackTrace { get; } + public string Type { get; } + public bool Is() + where TException : System.Exception { } + public override string ToString() { } + } + public abstract class ServerTransport + { + public int ConcurrentAccepts { get; set; } + public byte MaxReceivedMessageSizeInMegabytes { get; set; } + } +} +namespace UiPath.Ipc.Transport.NamedPipe +{ + public sealed class NamedPipeClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeClientTransport() { } + public bool AllowImpersonation { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override string ToString() { } + } + public sealed class NamedPipeServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeServerTransport() { } + [Newtonsoft.Json.JsonIgnore] + public System.Action? AccessControl { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override string ToString() { } + } +} +namespace UiPath.Ipc.Transport.Tcp +{ + public sealed class TcpClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpClientTransport() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override string ToString() { } + } + public sealed class TcpServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpServerTransport() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override string ToString() { } + } +} +namespace UiPath.Ipc.Transport.WebSocket +{ + public sealed class WebSocketClientTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketClientTransport() { } + public System.Uri Uri { get; init; } + public override string ToString() { } + } + public sealed class WebSocketServerTransport : UiPath.Ipc.ServerTransport + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketServerTransport() { } + public System.Func> Accept { get; init; } + public override string ToString() { } + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/report/generate-report.bat b/src/UiPath.CoreIpc/report/generate-report.bat new file mode 100644 index 00000000..9bceba9b --- /dev/null +++ b/src/UiPath.CoreIpc/report/generate-report.bat @@ -0,0 +1,41 @@ +@echo OFF +REM ******************************* +REM Make sure you have: +REM 1. Installed the .NET Global Tool first. +REM More information: https://github.com/PublicApiGenerator/PublicApiGenerator?tab=readme-ov-file#install +REM --- +REM dotnet tool install -g PublicApiGenerator.Tool +REM --- +REM 2. Built CoreIpc.sln. +REM ******************************* + +echo ******************************* +echo Make sure you have: +echo 1. Installed the .NET Global Tool first. +echo More information: https://github.com/PublicApiGenerator/PublicApiGenerator?tab=readme-ov-file#install +echo --- +echo dotnet tool install -g PublicApiGenerator.Tool +echo --- +echo 2. Built CoreIpc.sln. + +set "outputPath=%~dp0" +set "outputPath=%outputPath:~0,-1%" :: trim the final backslash + +set "projectPath=%~dp0..\" +pushd "%projectPath%" >nul 2>&1 +if %errorlevel% neq 0 ( + echo Invalid path + exit /b 1 +) +set "projectPath=%CD%\UiPath.CoreIpc.csproj" +popd + +echo ON + +REM generate-public-api --target-frameworks "net6.0-windows" --assembly UiPath.Ipc.dll --project-path "%projectPath%" --output-directory "%outputPath%" +REM generate-public-api --target-frameworks "net6.0" --assembly UiPath.Ipc.dll --project-path "%projectPath%" --output-directory "%outputPath%" +REM generate-public-api --target-frameworks "net461" --assembly UiPath.Ipc.dll --project-path "%projectPath%" --output-directory "%outputPath%" + +generate-public-api --target-frameworks "net6.0-windows" --assembly UiPath.Ipc.dll --project-path "%projectPath%" --output-directory "%outputPath%" --verbose --leave-artifacts +REM generate-public-api --target-frameworks "net6.0" --assembly UiPath.Ipc.dll --project-path "D:\Alt\coreipc\src\UiPath.CoreIpc\UiPath.CoreIpc.csproj" --output-directory "D:\Alt\coreipc\src\UiPath.CoreIpc\report" --verbose +REM generate-public-api --target-frameworks "net461" --assembly UiPath.Ipc.dll --project-path "D:\Alt\coreipc\src\UiPath.CoreIpc\UiPath.CoreIpc.csproj" --output-directory "D:\Alt\coreipc\src\UiPath.CoreIpc\report" --verbose