From 6b263e6b7868a6a2178341616492fdc61c5cd1b8 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 9 Oct 2024 15:27:50 +0200 Subject: [PATCH 01/26] Changes: -------- 1. Pipelines - (TBR) disable the constraint of pushing NuGet packages to the feed only when source branch is master - (TBD) restore, build and pack the main solution explicitly before running unit tests - Use .NET Runtime 8.0.8 and UseDotNet@2 for both 6.0.317 and 8.0.400 2. General - change the UiPath.CoreIpc* namespace to UiPath.Ipc* - decomission the usage of Nito - start optimizing logging via LoggerMessageAttribute 3. API and behavior - commission the new Ipc API that doesn't rely on the fluent builder pattern - commission the feature that allows the optional BeforeCall of an IpcServer Endpoint to share its AsyncLocal context with the target method execution, if the BeforeCall executes synchronously. Check the ServerBeforeCall_WhenSync_ShouldShareAsyncLocalContextWithTheTargetMethodCall test for further details. - extensibility support for potentially any transport, including JS-DotNetWasm interop 4. Internals - decommission the generic variant of ServiceClient, thus greatly simplifying the flow - commission the ServiceClientProper and ServiceClientForCallback subclasses --- src/NuGet.Config => NuGet.Config | 1 + src/CI/azp-dotnet-dist.yaml | 4 +- src/CI/azp-dotnet.yaml | 6 + src/CI/azp-initialization.yaml | 19 + src/CI/azp-nodejs.yaml | 2 +- src/CI/azp-start.yaml | 50 +- .../js/dotnet/UiPath.CoreIpc.NodeInterop.sln | 4 +- .../UiPath.CoreIpc.NodeInterop/Contracts.cs | 2 +- .../UiPath.CoreIpc.NodeInterop/Program.cs | 146 +++--- .../ServiceImpls.cs | 2 +- .../UiPath.CoreIpc.NodeInterop/Signalling.cs | 2 +- src/CoreIpc.sln | 41 +- src/Directory.Build.targets | 5 + src/IpcSample.ConsoleClient/Client.cs | 17 +- .../IpcSample.ConsoleClient.csproj | 6 +- src/IpcSample.ConsoleClient/SimpleClient.cs | 66 +++ src/IpcSample.ConsoleClient/TcpClient.cs | 9 +- .../WebSocketClient.cs | 16 +- src/IpcSample.ConsoleServer/Server.cs | 14 +- src/IpcSample.ConsoleServer/TcpServer.cs | 18 +- .../WebSocketServer.cs | 16 +- src/Playground/Contracts.cs | 24 + src/Playground/Impl.cs | 68 +++ src/Playground/Playground.csproj | 21 + src/Playground/Program.cs | 150 ++++++ src/UiPath.CoreIpc.Http/BidiHttpListener.cs | 285 ++++++++++++ src/UiPath.CoreIpc.Http/Constants.cs | 8 + src/UiPath.CoreIpc.Http/GlobalUsings.cs | 1 + .../UiPath.CoreIpc.Http.csproj | 27 ++ src/UiPath.CoreIpc.Tests/ComputingTests.cs | 129 ------ src/UiPath.CoreIpc.Tests/EndpointTests.cs | 141 ------ .../Implementation/ComputingCallback.cs | 18 - .../Implementation/ComputingService.cs | 136 ------ .../Implementation/IpcHelpers.cs | 77 ---- .../Implementation/OneWayStreamWrapper.cs | 104 ----- .../Implementation/SystemService.cs | 175 ------- src/UiPath.CoreIpc.Tests/NamedPipeTests.cs | 53 --- src/UiPath.CoreIpc.Tests/NestedStreamTests.cs | 370 --------------- src/UiPath.CoreIpc.Tests/SystemTests.cs | 295 ------------ src/UiPath.CoreIpc.Tests/TcpTests..cs | 28 -- src/UiPath.CoreIpc.Tests/TestBase.cs | 39 -- .../UiPath.CoreIpc.Tests.csproj | 30 -- src/UiPath.CoreIpc.Tests/ValidationTests.cs | 48 -- src/UiPath.CoreIpc.Tests/WebSocketTests.cs | 49 -- src/UiPath.CoreIpc/Client/CallInfo.cs | 14 + .../Client/ClientConnectionsRegistry.cs | 98 ---- src/UiPath.CoreIpc/Client/IpcProxy.cs | 21 + src/UiPath.CoreIpc/Client/ServiceClient.cs | 411 ++++++++--------- .../Client/ServiceClientBuilder.cs | 110 ----- src/UiPath.CoreIpc/Config/ClientConfig.cs | 60 +++ src/UiPath.CoreIpc/Config/ClientTransport.cs | 7 + .../Config/EndpointCollection.cs | 21 + src/UiPath.CoreIpc/Config/EndpointConfig.cs | 10 + src/UiPath.CoreIpc/Config/IListenerConfig.cs | 12 + .../Config/IServiceClientConfig.cs | 11 + src/UiPath.CoreIpc/Config/IpcClient.cs | 35 ++ src/UiPath.CoreIpc/Config/IpcServer.cs | 96 ++++ src/UiPath.CoreIpc/Config/ListenerConfig.cs | 31 ++ src/UiPath.CoreIpc/Connection.cs | 156 +++++-- src/UiPath.CoreIpc/GlobalSuppressions.cs | 6 - src/UiPath.CoreIpc/GlobalUsings.cs | 7 + .../CancellationTokenSourcePool.cs | 2 +- .../Helpers/DefaultsExtensions.cs | 28 ++ src/UiPath.CoreIpc/Helpers/FastAsyncLock.cs | 14 + src/UiPath.CoreIpc/{ => Helpers}/Helpers.cs | 114 +++-- .../{ => Helpers}/NestedStream.cs | 4 +- src/UiPath.CoreIpc/Helpers/Router.cs | 140 ++++++ .../{ => Helpers}/TaskCompletionPool.cs | 2 +- src/UiPath.CoreIpc/IpcJsonSerializer.cs | 59 --- .../Logging/LoggingExtensions.cs | 72 +++ .../NamedPipe/NamedPipeClient.cs | 51 --- .../NamedPipe/NamedPipeClientBuilder.cs | 53 --- .../NamedPipe/NamedPipeListener.cs | 55 --- .../CallerArgumentExpressionAttribute.cs | 36 ++ .../Polyfills/CancellationTokenExtensions.cs | 11 + .../Polyfills/CollectionExtensions.cs | 19 + .../CompilerFeatureRequiredAttribute.cs | 56 +++ .../Polyfills/EnumerableExtensions.cs | 17 + .../Polyfills/IsExternalInit.cs | 19 + .../Polyfills/MemberNotNullWhenAttribute.cs | 71 +++ .../Polyfills/NotNullIfNotNullAttribute.cs | 41 ++ .../Polyfills/NotNullWhenAttribute.cs | 38 ++ .../Polyfills/RequiredMemberAttribute.cs | 29 ++ .../Polyfills/SetsRequiredMembersAttribute.cs | 20 + src/UiPath.CoreIpc/Polyfills/System_Index.cs | 151 ++++++ .../Polyfills/TcpClientExtensions.cs | 14 + src/UiPath.CoreIpc/Server/EndpointSettings.cs | 58 +++ src/UiPath.CoreIpc/Server/Listener.cs | 250 +++++++--- src/UiPath.CoreIpc/Server/Server.cs | 231 ++++++---- src/UiPath.CoreIpc/Server/ServerConnection.cs | 160 +++++-- src/UiPath.CoreIpc/Server/ServiceHost.cs | 34 -- .../Server/ServiceHostBuilder.cs | 74 --- src/UiPath.CoreIpc/Tcp/TcpClient.cs | 42 -- src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs | 36 -- src/UiPath.CoreIpc/Tcp/TcpListener.cs | 47 -- .../Transport/NamedPipe/NamedPipeListener.cs | 72 +++ .../Transport/NamedPipe/NamedPipeTransport.cs | 51 +++ .../Transport/Tcp/TcpListener.cs | 61 +++ .../Transport/Tcp/TcpTransport.cs | 56 +++ .../Transport/WebSocket/WebSocketListener.cs | 38 ++ .../WebSocket}/WebSocketStream.cs | 10 +- .../Transport/WebSocket/WebSocketTransport.cs | 39 ++ src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 31 +- .../WebSockets/WebSocketClient.cs | 38 -- .../WebSockets/WebSocketClientBuilder.cs | 27 -- .../WebSockets/WebSocketListener.cs | 24 - src/UiPath.CoreIpc/{ => Wire}/Dtos.cs | 53 ++- .../Wire/EndpointNotFoundException.cs | 17 + src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs | 63 +++ .../UiPath.Ipc.net6.0-windows.received.txt | 264 +++++++++++ src/UiPath.CoreIpc/report/generate-report.bat | 41 ++ src/UiPath.Ipc.Tests/ComputingTests.cs | 327 +++++++++++++ .../ComputingTestsOverNamedPipes.cs | 32 ++ src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs | 35 ++ .../ComputingTestsOverWebSockets.cs | 44 ++ .../Config/OverrideConfigAttribute.cs | 39 ++ src/UiPath.Ipc.Tests/GlobalUsings.cs | 3 + src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs | 17 + .../Helpers/HttpSysWebSocketsListener.cs | 72 +++ .../Helpers/IpcAutoDataAttribute.cs | 19 + src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs | 57 +++ src/UiPath.Ipc.Tests/Helpers/Names.cs | 8 + src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs | 14 + .../Helpers/ShouldlyHelpers.cs | 117 +++++ src/UiPath.Ipc.Tests/Helpers/StreamBase.cs | 15 + .../Helpers/StreamExtensions.cs | 27 ++ src/UiPath.Ipc.Tests/Helpers/TestRunId.cs | 32 ++ src/UiPath.Ipc.Tests/Helpers/Timeouts.cs | 12 + src/UiPath.Ipc.Tests/Helpers/TracedStream.cs | 16 + .../Helpers/WebSocketContext.cs | 30 ++ src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs | 79 ++++ .../CallerArgumentExpressionAttribute.cs | 36 ++ .../Polyfills/IsExternalInit.cs | 19 + .../Polyfills/System_Index.cs | 151 ++++++ src/UiPath.Ipc.Tests/Program.cs | 33 ++ src/UiPath.Ipc.Tests/RobotTests.cs | 72 +++ .../RobotTestsOverNamedPipes.cs | 293 ++++++++++++ .../Services/ArithmeticCallback.cs | 7 + .../Services/ComputingCallback.cs | 11 + .../Services/ComputingService.cs | 66 +++ .../Services/IComputingService.cs | 48 ++ .../Services/ISystemService.cs | 45 ++ .../Services/Robot/Contracts.cs | 36 ++ src/UiPath.Ipc.Tests/Services/Robot/Impl.cs | 72 +++ src/UiPath.Ipc.Tests/Services/Robot/Pals.cs | 97 ++++ .../Services/SystemService.cs | 82 ++++ src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs | 147 ++++++ src/UiPath.Ipc.Tests/SystemTests.cs | 433 ++++++++++++++++++ .../SystemTestsOverNamedPipes.cs | 21 + src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs | 21 + .../SystemTestsOverWebSockets.cs | 31 ++ src/UiPath.Ipc.Tests/TestBase.cs | 160 +++++++ src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj | 56 +++ .../Xunit/CustomTestFramework.cs | 120 +++++ 154 files changed, 6668 insertions(+), 3145 deletions(-) rename src/NuGet.Config => NuGet.Config (56%) create mode 100644 src/Directory.Build.targets create mode 100644 src/IpcSample.ConsoleClient/SimpleClient.cs create mode 100644 src/Playground/Contracts.cs create mode 100644 src/Playground/Impl.cs create mode 100644 src/Playground/Playground.csproj create mode 100644 src/Playground/Program.cs create mode 100644 src/UiPath.CoreIpc.Http/BidiHttpListener.cs create mode 100644 src/UiPath.CoreIpc.Http/Constants.cs create mode 100644 src/UiPath.CoreIpc.Http/GlobalUsings.cs create mode 100644 src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj delete mode 100644 src/UiPath.CoreIpc.Tests/ComputingTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/EndpointTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs delete mode 100644 src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs delete mode 100644 src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs delete mode 100644 src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs delete mode 100644 src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs delete mode 100644 src/UiPath.CoreIpc.Tests/NamedPipeTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/NestedStreamTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/SystemTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/TcpTests..cs delete mode 100644 src/UiPath.CoreIpc.Tests/TestBase.cs delete mode 100644 src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj delete mode 100644 src/UiPath.CoreIpc.Tests/ValidationTests.cs delete mode 100644 src/UiPath.CoreIpc.Tests/WebSocketTests.cs create mode 100644 src/UiPath.CoreIpc/Client/CallInfo.cs delete mode 100644 src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs create mode 100644 src/UiPath.CoreIpc/Client/IpcProxy.cs delete mode 100644 src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs create mode 100644 src/UiPath.CoreIpc/Config/ClientConfig.cs create mode 100644 src/UiPath.CoreIpc/Config/ClientTransport.cs create mode 100644 src/UiPath.CoreIpc/Config/EndpointCollection.cs create mode 100644 src/UiPath.CoreIpc/Config/EndpointConfig.cs create mode 100644 src/UiPath.CoreIpc/Config/IListenerConfig.cs create mode 100644 src/UiPath.CoreIpc/Config/IServiceClientConfig.cs create mode 100644 src/UiPath.CoreIpc/Config/IpcClient.cs create mode 100644 src/UiPath.CoreIpc/Config/IpcServer.cs create mode 100644 src/UiPath.CoreIpc/Config/ListenerConfig.cs create mode 100644 src/UiPath.CoreIpc/GlobalUsings.cs rename src/UiPath.CoreIpc/{ => Helpers}/CancellationTokenSourcePool.cs (98%) create mode 100644 src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs create mode 100644 src/UiPath.CoreIpc/Helpers/FastAsyncLock.cs rename src/UiPath.CoreIpc/{ => Helpers}/Helpers.cs (76%) rename src/UiPath.CoreIpc/{ => Helpers}/NestedStream.cs (98%) create mode 100644 src/UiPath.CoreIpc/Helpers/Router.cs rename src/UiPath.CoreIpc/{ => Helpers}/TaskCompletionPool.cs (98%) delete mode 100644 src/UiPath.CoreIpc/IpcJsonSerializer.cs create mode 100644 src/UiPath.CoreIpc/Logging/LoggingExtensions.cs delete mode 100644 src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs delete mode 100644 src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs delete mode 100644 src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/CancellationTokenExtensions.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/CompilerFeatureRequiredAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/IsExternalInit.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/NotNullIfNotNullAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/NotNullWhenAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/RequiredMemberAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/SetsRequiredMembersAttribute.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/System_Index.cs create mode 100644 src/UiPath.CoreIpc/Polyfills/TcpClientExtensions.cs create mode 100644 src/UiPath.CoreIpc/Server/EndpointSettings.cs delete mode 100644 src/UiPath.CoreIpc/Server/ServiceHost.cs delete mode 100644 src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs delete mode 100644 src/UiPath.CoreIpc/Tcp/TcpClient.cs delete mode 100644 src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs delete mode 100644 src/UiPath.CoreIpc/Tcp/TcpListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs create mode 100644 src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs create mode 100644 src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs rename src/UiPath.CoreIpc/{WebSockets => Transport/WebSocket}/WebSocketStream.cs (93%) create mode 100644 src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs delete mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs delete mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs delete mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs rename src/UiPath.CoreIpc/{ => Wire}/Dtos.cs (59%) create mode 100644 src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs create mode 100644 src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs create mode 100644 src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt create mode 100644 src/UiPath.CoreIpc/report/generate-report.bat create mode 100644 src/UiPath.Ipc.Tests/ComputingTests.cs create mode 100644 src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs create mode 100644 src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs create mode 100644 src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs create mode 100644 src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs create mode 100644 src/UiPath.Ipc.Tests/GlobalUsings.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/Names.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/ShouldlyHelpers.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/StreamBase.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/StreamExtensions.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/TestRunId.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/Timeouts.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/TracedStream.cs create mode 100644 src/UiPath.Ipc.Tests/Helpers/WebSocketContext.cs create mode 100644 src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs create mode 100644 src/UiPath.Ipc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs create mode 100644 src/UiPath.Ipc.Tests/Polyfills/IsExternalInit.cs create mode 100644 src/UiPath.Ipc.Tests/Polyfills/System_Index.cs create mode 100644 src/UiPath.Ipc.Tests/Program.cs create mode 100644 src/UiPath.Ipc.Tests/RobotTests.cs create mode 100644 src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs create mode 100644 src/UiPath.Ipc.Tests/Services/ArithmeticCallback.cs create mode 100644 src/UiPath.Ipc.Tests/Services/ComputingCallback.cs create mode 100644 src/UiPath.Ipc.Tests/Services/ComputingService.cs create mode 100644 src/UiPath.Ipc.Tests/Services/IComputingService.cs create mode 100644 src/UiPath.Ipc.Tests/Services/ISystemService.cs create mode 100644 src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs create mode 100644 src/UiPath.Ipc.Tests/Services/Robot/Impl.cs create mode 100644 src/UiPath.Ipc.Tests/Services/Robot/Pals.cs create mode 100644 src/UiPath.Ipc.Tests/Services/SystemService.cs create mode 100644 src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs create mode 100644 src/UiPath.Ipc.Tests/SystemTests.cs create mode 100644 src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs create mode 100644 src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs create mode 100644 src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs create mode 100644 src/UiPath.Ipc.Tests/TestBase.cs create mode 100644 src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj create mode 100644 src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs diff --git a/src/NuGet.Config b/NuGet.Config similarity index 56% rename from src/NuGet.Config rename to NuGet.Config index ba6d1755..437e25e1 100644 --- a/src/NuGet.Config +++ b/NuGet.Config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/src/CI/azp-dotnet-dist.yaml b/src/CI/azp-dotnet-dist.yaml index bafcc806..02e66263 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' @@ -24,7 +24,7 @@ steps: - task: PublishSymbols@2 displayName: 'Publish Symbols to UiPath Azure Artifacts Symbol Server' - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) + condition: succeeded() inputs: symbolsFolder: $(Build.SourcesDirectory) searchPattern: '**/UiPath.CoreIpc/bin/**/UiPath.CoreIpc.pdb' diff --git a/src/CI/azp-dotnet.yaml b/src/CI/azp-dotnet.yaml index 0af4fc84..515a8ea1 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: 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..d51e5497 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-20.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..bb5ed827 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs @@ -7,14 +7,15 @@ 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 +62,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 +77,6 @@ static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSec var sp = services .AddLogging() - .AddIpc() .AddSingleton() .AddSingleton() .AddSingleton() @@ -79,18 +84,27 @@ 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 ipcServer = new IpcServer() + { + Endpoints = new() + { + typeof(IAlgebra), + typeof(ICalculus), + typeof(IBrittleService), + typeof(IEnvironmentVariableGetter), + typeof(IDtoService) + }, + Listeners = [ + ..EnumerateListeners(pipeName, webSocketUrl) + ], + ServiceProvider = sp, + Scheduler = scheduler + }; + ipcServer.Start(); _ = Task.Run(async () => { @@ -98,7 +112,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 +120,51 @@ 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 + { + Config = new() + { + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() + { + { typeof(IArithmetic), callback } + }, + }, + Transport = new WebSocketTransport + { + 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 + { + Config = new() + { + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() + { + { typeof(IArithmetic), callback } + } + }, + Transport = new NamedPipeTransport() + { + PipeName = pipeName, + } + } + .GetProxy() + .Ping(); } } await Task.WhenAll(EnumeratePings()); - + Send(SignalKind.ReadyToConnect); } catch (Exception ex) @@ -135,7 +174,29 @@ IEnumerable EnumeratePings() } }); - await serviceHost.RunAsync(sched); + await ipcServer.WaitForStop(); + + IEnumerable EnumerateListeners(string? pipeName, string? webSocketUrl) + { + if (pipeName is not null) + { + yield return new NamedPipeListener() { PipeName = pipeName }; + } + + if (webSocketUrl is not null) + { + string url = CurateWebSocketUrl(webSocketUrl); + var accept = new HttpSysWebSocketsListener(url).Accept; + yield return new WebSocketListener() { Accept = accept }; + } + + static string CurateWebSocketUrl(string raw) + { + var builder = new UriBuilder(raw); + builder.Scheme = "http"; + return builder.ToString(); + } + } } private class Arithmetic : IArithmetic @@ -148,37 +209,6 @@ private class Arithmetic : IArithmetic 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(); - } - - if (pipeName is not null) - { - builder = builder.UseNamedPipes(new NamedPipeSettings(pipeName)); - } - - if (webSocketUrl is not null) - { - string url = CurateWebSocketUrl(webSocketUrl); - var accept = new HttpSysWebSocketsListener(url).Accept; - WebSocketSettings settings = new(accept); - - builder = builder.UseWebSockets(settings); - } - - return builder; - } - - private static string CurateWebSocketUrl(string raw) - { - var builder = new UriBuilder(raw); - builder.Scheme = "http"; - return builder.ToString(); - } - 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..3b9706f1 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; 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..12d4a07d 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -1,43 +1,44 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 +# 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.targets = Directory.Build.targets + ..\NuGet.Config = ..\NuGet.Config EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "Playground\Playground.csproj", "{F0365E40-DA73-4583-A363-89CBEF68A4C6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc.Http", "UiPath.CoreIpc.Http\UiPath.CoreIpc.Http.csproj", "{8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.Ipc.Tests", "UiPath.Ipc.Tests\UiPath.Ipc.Tests.csproj", "{E238E183-92CF-48A6-890F-C422853D6656}" +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 + {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Release|Any CPU.Build.0 = Release|Any CPU + {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E238E183-92CF-48A6-890F-C422853D6656}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E238E183-92CF-48A6-890F-C422853D6656}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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..b715e1cb 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -1,9 +1,9 @@ -using System.Text; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.NamedPipe; -using Microsoft.Extensions.DependencyInjection; +using System.Text; +using UiPath.Ipc.BackCompat; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; class Client { @@ -32,7 +32,7 @@ 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)); + .CallbackInstance(callback).AllowImpersonation().RequestTimeout(TimeSpan.FromSeconds(2)); var stopwatch = Stopwatch.StartNew(); int count = 0; try @@ -40,7 +40,6 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) var computingClient = computingClientBuilder.ValidateAndBuild(); var systemClient = new NamedPipeClientBuilder("test") - .SerializeParametersAsObjects() .RequestTimeout(TimeSpan.FromSeconds(2)) .Logger(serviceProvider) .AllowImpersonation() @@ -67,7 +66,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) Console.WriteLine($"[TEST 3] sum of 3 complexe number is: {result3.A}+{result3.B}i", cancellationToken); // test 4: call IPC service method without parameter or return - await systemClient.DoNothing(cancellationToken); + await systemClient.FireAndForget(cancellationToken); Console.WriteLine($"[TEST 4] invoked DoNothing()"); //((IDisposable)systemClient).Dispose(); @@ -76,7 +75,7 @@ private static async Task RunTestsAsync(CancellationToken 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.EchoGuid(Guid.NewGuid(), cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array @@ -109,7 +108,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 diff --git a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj index a313c304..646a29aa 100644 --- a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj +++ b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj @@ -6,10 +6,10 @@ app1.manifest latest true + enable - @@ -18,5 +18,9 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/IpcSample.ConsoleClient/SimpleClient.cs b/src/IpcSample.ConsoleClient/SimpleClient.cs new file mode 100644 index 00000000..eba71a10 --- /dev/null +++ b/src/IpcSample.ConsoleClient/SimpleClient.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using UiPath.Ipc.Tests; + +namespace IpcSample.ConsoleClient; + +internal class SimpleClient +{ + + public static async Task Entry() + { + Settings pf = new() + { + ClientTransport = new ClientTransport.NamedPipes + { + PipeName = "test", + AllowImpersonation = true + }, + Logger = new Logger(new LoggerFactory()), + RequestTimeout = TimeSpan.FromSeconds(2), + Callback = new CallbackSource.Instance + { + CallbackInstance = new ComputingCallback { Id = "custom made" } + } + }; + + var cs = pf.Build(); + // ----------- + } + + class Settings + { + public required ClientTransport ClientTransport { get; init; } + public TimeSpan RequestTimeout { get; init; } = Timeout.InfiniteTimeSpan; + public required ILogger Logger { get; init; } + public CallbackSource? Callback { get; init; } + + public T Build() where T : class + { + throw null!; + } + } + + abstract class CallbackSource + { + public class Injected : CallbackSource + { + public required IServiceProvider ServiceProvider { get; init; } + public required Type CallbackType { get; init; } + } + + public class Instance : CallbackSource + { + public required object CallbackInstance { get; init; } + } + } + + abstract class ClientTransport + { + public class NamedPipes : ClientTransport + { + public required string PipeName { get; init; } + public bool AllowImpersonation { get; init; } + } + } + +} diff --git a/src/IpcSample.ConsoleClient/TcpClient.cs b/src/IpcSample.ConsoleClient/TcpClient.cs index 0768ebc6..0e5b33ed 100644 --- a/src/IpcSample.ConsoleClient/TcpClient.cs +++ b/src/IpcSample.ConsoleClient/TcpClient.cs @@ -1,10 +1,10 @@ using System.Text; using System.Diagnostics; -using UiPath.CoreIpc.Tcp; +using UiPath.Ipc.BackCompat; using Microsoft.Extensions.DependencyInjection; using System.Net; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; class TcpClient { @@ -35,7 +35,7 @@ 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)); + .CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); var stopwatch = Stopwatch.StartNew(); int count = 0; try @@ -43,7 +43,6 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) var computingClient = computingClientBuilder.ValidateAndBuild(); var systemClient = new TcpClientBuilder(SystemEndPoint) - .SerializeParametersAsObjects() //.EncryptAndSign("localhost") .RequestTimeout(TimeSpan.FromSeconds(2)) .Logger(serviceProvider) @@ -84,7 +83,7 @@ private static async Task RunTestsAsync(CancellationToken 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.EchoGuid(Guid.NewGuid(), cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array diff --git a/src/IpcSample.ConsoleClient/WebSocketClient.cs b/src/IpcSample.ConsoleClient/WebSocketClient.cs index e6c93422..c9e1afbc 100644 --- a/src/IpcSample.ConsoleClient/WebSocketClient.cs +++ b/src/IpcSample.ConsoleClient/WebSocketClient.cs @@ -1,8 +1,10 @@ -using System.Text; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.WebSockets; -using Microsoft.Extensions.DependencyInjection; -namespace UiPath.CoreIpc.Tests; +using System.Text; +using UiPath.Ipc.BackCompat; + +namespace UiPath.Ipc.Tests; + class WebSocketClient { static async Task _Main(string[] args) @@ -32,7 +34,7 @@ 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() + var computingClientBuilder = new WebSocketClientBuilder(uri, serviceProvider) .CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); var stopwatch = Stopwatch.StartNew(); int count = 0; @@ -40,7 +42,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) { var computingClient = computingClientBuilder.ValidateAndBuild(); var systemClient = - new WebSocketClientBuilder(uri).SerializeParametersAsObjects() + new WebSocketClientBuilder(uri) //.EncryptAndSign("localhost") .RequestTimeout(TimeSpan.FromSeconds(2)) .Logger(serviceProvider) @@ -81,7 +83,7 @@ private static async Task RunTestsAsync(CancellationToken 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.EchoGuid(Guid.NewGuid(), cancellationToken); Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); // test 7: call IPC service method with byte array diff --git a/src/IpcSample.ConsoleServer/Server.cs b/src/IpcSample.ConsoleServer/Server.cs index 8e9ad9e4..87e6c9ca 100644 --- a/src/IpcSample.ConsoleServer/Server.cs +++ b/src/IpcSample.ConsoleServer/Server.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.CoreIpc.NamedPipe; +using UiPath.Ipc.BackCompat; +using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; class Server { @@ -21,20 +22,21 @@ static async Task Main() var serviceProvider = ConfigureServices(); // build and run service host var host = new ServiceHostBuilder(serviceProvider) - .UseNamedPipes(new NamedPipeSettings("test") + .UseNamedPipes(new NamedPipeListener() { + PipeName = "test", RequestTimeout = TimeSpan.FromSeconds(2), //AccessControl = security => security.AllowCurrentUser(), }) - .AddEndpoint() + .AddEndpoint() .AddEndpoint() .ValidateAndBuild(); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + await await Task.WhenAny(host.RunAsync(), Task.Run(async () => { Console.WriteLine(typeof(int).Assembly); Console.ReadLine(); - host.Dispose(); + await host.DisposeAsync(); })); Console.WriteLine("Server stopped."); diff --git a/src/IpcSample.ConsoleServer/TcpServer.cs b/src/IpcSample.ConsoleServer/TcpServer.cs index 172a7725..a89cec24 100644 --- a/src/IpcSample.ConsoleServer/TcpServer.cs +++ b/src/IpcSample.ConsoleServer/TcpServer.cs @@ -1,9 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using System.Net; -using UiPath.CoreIpc.Tcp; +using UiPath.Ipc.BackCompat; +using UiPath.Ipc.Transport.Tcp; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; + +using IPEndPoint = System.Net.IPEndPoint; +using IPAddress = System.Net.IPAddress; class TcpServer { @@ -25,20 +28,21 @@ static async Task _Main() // build and run service host var data = File.ReadAllBytes(@"../../../../localhost.pfx"); var host = new ServiceHostBuilder(serviceProvider) - .UseTcp(new TcpSettings(SystemEndPoint) + .UseTcp(new TcpListener() { + EndPoint = SystemEndPoint, RequestTimeout = TimeSpan.FromSeconds(2), //Certificate = new X509Certificate(data, "1"), }) - .AddEndpoint() + .AddEndpoint() .AddEndpoint() .ValidateAndBuild(); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + await await Task.WhenAny(host.RunAsync(), Task.Run(async () => { Console.WriteLine(typeof(int).Assembly); Console.ReadLine(); - host.Dispose(); + await host.DisposeAsync(); })); Console.WriteLine("Server stopped."); diff --git a/src/IpcSample.ConsoleServer/WebSocketServer.cs b/src/IpcSample.ConsoleServer/WebSocketServer.cs index 322c1d65..8a52c013 100644 --- a/src/IpcSample.ConsoleServer/WebSocketServer.cs +++ b/src/IpcSample.ConsoleServer/WebSocketServer.cs @@ -1,9 +1,8 @@ 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.BackCompat; +using UiPath.Ipc.Transport.WebSocket; +namespace UiPath.Ipc.Tests; class WebSocketServer { //private static readonly Timer _timer = new Timer(_ => @@ -23,19 +22,20 @@ static async Task _Main() // 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) + .UseWebSockets(new WebSocketListener() { + Accept = new HttpSysWebSocketsListener("http://localhost:1212/wsDemo/").Accept, RequestTimeout = TimeSpan.FromSeconds(2), //Certificate = new X509Certificate(data, "1"), }) - .AddEndpoint() + .AddEndpoint() .AddEndpoint() .ValidateAndBuild(); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + await await Task.WhenAny(host.RunAsync(), Task.Run(async () => { Console.WriteLine(typeof(int).Assembly); Console.ReadLine(); - host.Dispose(); + await host.DisposeAsync(); })); Console.WriteLine("Server stopped."); return; 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..4d020920 --- /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!.GetCallback(); + var clientOps2 = m.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..760afbc7 --- /dev/null +++ b/src/Playground/Playground.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs new file mode 100644 index 00000000..86bb8470 --- /dev/null +++ b/src/Playground/Program.cs @@ -0,0 +1,150 @@ +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, + Endpoints = new() + { + typeof(Contracts.IServerOperations), // DEVINE + new EndpointSettings(typeof(Contracts.IServerOperations)) // ASTALALT + { + BeforeCall = async (callInfo, _) => + { + Console.WriteLine($"Server: {callInfo.Method.Name}"); + } + }, + typeof(Contracts.IClientOperations2) + }, + Listeners = [ + new NamedPipeListener() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AccessControl = ps => + { + }, + MaxReceivedMessageSizeInMegabytes = 100, + RequestTimeout = TimeSpan.FromHours(10) + }, + //new BidirectionalHttp.ListenerConfig() + //{ + // Uri = serverUri, + // RequestTimeout = TimeSpan.FromHours(1) + //} + ] + }; + + 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() + { + Config = new() + { + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + ServiceProvider = clientSP, + Scheduler = clientScheduler, + }, + Transport = new NamedPipeTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AllowImpersonation = false, + }, + }; + + var c2 = new IpcClient() + { + Config = new() + { + ServiceProvider = clientSP, + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + Scheduler = clientScheduler, + }, + Transport = new NamedPipeTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AllowImpersonation = false, + }, + }; + + var proxy1 = new IpcClient() + { + Config = new() + { + ServiceProvider = clientSP, + Callbacks = new() + { + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, + }, + Scheduler = clientScheduler, + }, + Transport = new NamedPipeTransport() + { + 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.Http/BidiHttpListener.cs b/src/UiPath.CoreIpc.Http/BidiHttpListener.cs new file mode 100644 index 00000000..e5c299df --- /dev/null +++ b/src/UiPath.CoreIpc.Http/BidiHttpListener.cs @@ -0,0 +1,285 @@ +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.Http; + +using static Constants; +using IBidiHttpListenerConfig = IListenerConfig; + +public sealed partial record BidiHttpListener : ListenerConfig, IBidiHttpListenerConfig +{ + public required Uri Uri { get; init; } + + BidiHttpListenerState IBidiHttpListenerConfig.CreateListenerState(IpcServer server) + => new(server, this); + + BidiHttpServerConnectionState IBidiHttpListenerConfig.CreateConnectionState(IpcServer server, BidiHttpListenerState listenerState) + => new(server, listenerState); + + async ValueTask IBidiHttpListenerConfig.AwaitConnection(BidiHttpListenerState listenerState, BidiHttpServerConnectionState connectionState, CancellationToken ct) + { + await connectionState.WaitForConnection(ct); + return connectionState; + } + + public IEnumerable Validate() + { + throw new NotImplementedException(); + } +} + +internal sealed class BidiHttpListenerState : IAsyncDisposable +{ + private readonly IpcServer _ipcServer; + 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 BidiHttpListenerState(IpcServer ipcServer, BidiHttpListener listener) + { + _ipcServer = ipcServer; + _httpListener = new HttpListener() + { + Prefixes = + { + listener.Uri.ToString() + } + }; + _processing = ProcessContexts(); + _disposing = new(DisposeCore); + } + + public 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; + } + } +} + +internal sealed class BidiHttpServerConnectionState : Stream, IAsyncDisposable +{ + private readonly Pipe _pipe = new(); + + private readonly IpcServer _server; + private readonly BidiHttpListenerState _listenerState; + + private readonly CancellationTokenSource _cts = new(); + private readonly AsyncLock _lock = new(); + private (Guid connectionId, Uri reverseUri)? _connection = null; + private HttpClient? _client; + private Task? _processing = null; + private readonly Lazy _disposing; + + public BidiHttpServerConnectionState(IpcServer server, BidiHttpListenerState listenerState) + { + _server = server; + _listenerState = listenerState; + _disposing = new(DisposeCore); + } + + public +#if !NET461 + override +#endif + ValueTask DisposeAsync() => new(_disposing.Value); + + 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 async Task WaitForConnection(CancellationToken ct) + { + using (await _lock.LockAsync(ct)) + { + if (_connection is not null) + { + throw new InvalidOperationException(); + } + + _connection = await _listenerState.NewConnections.ReadAsync(ct); + + _client = new() + { + BaseAddress = _connection.Value.reverseUri, + DefaultRequestHeaders = + { + { ConnectionIdHeader, _connection.Value.connectionId.ToString() } + } + }; + + _processing = ProcessContexts(_cts.Token); + } + } + + private async Task ProcessContexts(CancellationToken ct) + { + var reader = _listenerState.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(); + } + } + } + + 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 _pipe.Reader.ReadAsync(ct); + + var take = (int)Math.Min(readResult.Buffer.Length, memory.Length); + + readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); + _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 (_client is null) + { + throw new InvalidOperationException(); + } + + HttpContent content = +#if NET461 + new ByteArrayContent(memory.ToArray()); +#else + new ReadOnlyMemoryContent(memory); +#endif + + await _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.Http/Constants.cs b/src/UiPath.CoreIpc.Http/Constants.cs new file mode 100644 index 00000000..c04e37b7 --- /dev/null +++ b/src/UiPath.CoreIpc.Http/Constants.cs @@ -0,0 +1,8 @@ +namespace UiPath.Ipc.Http; + +internal static class Constants +{ + internal const string ConnectionIdHeader = "X-UiPathIpc-ConnectionId"; + internal const string ReverseUriHeader = "X-UiPathIpc-ReverseUri"; +} + diff --git a/src/UiPath.CoreIpc.Http/GlobalUsings.cs b/src/UiPath.CoreIpc.Http/GlobalUsings.cs new file mode 100644 index 00000000..a7770972 --- /dev/null +++ b/src/UiPath.CoreIpc.Http/GlobalUsings.cs @@ -0,0 +1 @@ +global using UiPath.Ipc.Extensibility; diff --git a/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj b/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj new file mode 100644 index 00000000..524ffbc1 --- /dev/null +++ b/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj @@ -0,0 +1,27 @@ + + + + net6.0;net461;net6.0-windows + enable + enable + preview + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/UiPath.CoreIpc.Tests/ComputingTests.cs b/src/UiPath.CoreIpc.Tests/ComputingTests.cs deleted file mode 100644 index 6c562dd9..00000000 --- a/src/UiPath.CoreIpc.Tests/ComputingTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace UiPath.CoreIpc.Tests; - -public abstract class ComputingTests : TestBase where TBuilder : ServiceClientBuilder -{ - protected readonly ServiceHost _computingHost; - protected readonly IComputingService _computingClient; - protected readonly ComputingService _computingService; - protected readonly ComputingCallback _computingCallback; - public ComputingTests() - { - _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(); - } - protected abstract TBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null); - [Fact] - public async Task ReconnectWithEncrypt() - { - for (int i = 0; i < 50; i++) - { - await _computingClient.AddFloat(1, 2); - ((IpcProxy)_computingClient).CloseConnection(); - await _computingClient.AddFloat(1, 2); - } - } - - [Fact] - public async Task AddFloat() - { - var result = await _computingClient.AddFloat(1.23f, 4.56f); - result.ShouldBe(5.79f); - } - - [Fact] - public Task AddFloatConcurrently() => Task.WhenAll(Enumerable.Range(1, 100).Select(_ => AddFloat())); - - [Fact] - public async Task AddComplexNumber() - { - var result = await _computingClient.AddComplexNumber(new ComplexNumber(1f, 3f), new ComplexNumber(2f, 5f)); - result.ShouldBe(new ComplexNumber(3f, 8f)); - } - - [Fact] - public async Task ClientCancellation() - { - using (var cancellationSource = new CancellationTokenSource(10)) - { - _computingClient.Infinite(cancellationSource.Token).ShouldThrow(); - } - await AddFloat(); - } - - [Fact] - public async Task ClientTimeout() - { - 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(); - } - - [Fact] - public async Task TimeoutPerRequest() - { - for (int i = 0; i < 20; i++) - { - var request = new SystemMessage { RequestTimeout = TimeSpan.FromTicks(10), Delay = 100 }; - Exception exception = null; - try - { - await _computingClient.SendMessage(request); - } - catch (TimeoutException ex) - { - exception = ex; - } - catch (RemoteException ex) - { - exception = ex; - ex.Is().ShouldBeTrue(); - } - exception.Message.ShouldBe($"{nameof(_computingClient.SendMessage)} timed out."); - await AddFloat(); - } - } - - [Fact] - public Task InfiniteVoid() => _computingClient.InfiniteVoid(); - - [Fact] - public async Task AddComplexNumbers() - { - 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)); - } - - [Fact] - public async Task GetCallbackThreadName() => (await _computingClient.GetCallbackThreadName()).ShouldBe("GuiThread"); - - [Fact] - public Task CallbackConcurrently() => Task.WhenAll(Enumerable.Range(1, 50).Select(_ => Callback())); - - [Fact] - public async Task Callback() - { - var message = new SystemMessage { Text = Guid.NewGuid().ToString() }; - var returnValue = await _computingClient.SendMessage(message); - returnValue.ShouldBe($"{Environment.UserName}_{_computingCallback.Id}_{message.Text}"); - } - - public override void Dispose() - { - ((IDisposable)_computingClient).Dispose(); - ((IpcProxy)_computingClient).CloseConnection(); - _computingHost.Dispose(); - base.Dispose(); - } -} \ 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/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/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/SystemTests.cs b/src/UiPath.CoreIpc.Tests/SystemTests.cs deleted file mode 100644 index e33832f4..00000000 --- a/src/UiPath.CoreIpc.Tests/SystemTests.cs +++ /dev/null @@ -1,295 +0,0 @@ -using System.Text; - -namespace UiPath.CoreIpc.Tests; - -public abstract class SystemTests : TestBase where TBuilder : ServiceClientBuilder -{ - 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) - { - base.Configure(listenerSettings); - listenerSettings.ConcurrentAccepts = 10; - listenerSettings.RequestTimeout = RequestTimeout.Subtract(TimeSpan.FromSeconds(1)); - return listenerSettings; - } - public override void Dispose() - { - ((IDisposable)_systemClient).Dispose(); - ((IpcProxy)_systemClient).CloseConnection(); - _systemHost.Dispose(); - base.Dispose(); - } - [Fact] - public async Task ConcurrentRequests() - { - var infinite = _systemClient.Infinite(); - await Guid(); - infinite.IsCompleted.ShouldBeFalse(); - } - [Fact] - public async Task OptionalMessage() - { - var returnValue = await _systemClient.ImpersonateCaller(); - returnValue.ShouldBe(Environment.UserName); - } - - [Fact] - public async Task ServerTimeout() - { - var ex = _systemClient.Infinite().ShouldThrow(); - ex.Message.ShouldBe($"{nameof(_systemClient.Infinite)} timed out."); - ex.Is().ShouldBeTrue(); - await Guid(); - } - [Fact] - public async Task Void() - { - _systemService.DidNothing = false; - await _systemClient.DoNothing(); - _systemService.DidNothing.ShouldBeFalse(); - while (!_systemService.DidNothing) - { - await Task.Delay(10); - Trace.WriteLine(this + " Void"); - } - } - - [Fact] - public async Task VoidThreadName() - { - await _systemClient.VoidThreadName(); - await _systemClient.GetThreadName(); - while (_systemService.ThreadName != "GuiThread") - { - await Task.Delay(0); - Trace.WriteLine(this + " VoidThreadName"); - } - } - - [Fact] - public async Task Enum() - { - var text = await _systemClient.ConvertText("hEllO woRd!", TextStyle.Upper); - text.ShouldBe("HELLO WORD!"); - } - - [Fact] - public async Task PropertyWithTypeDefaultValue() - { - var args = new ConvertTextArgs { Text = "hEllO woRd!", TextStyle = default }; - var text = await _systemClient.ConvertTextWithArgs(args); - text.ShouldBe("Hello Word!"); - } - - [Fact] - public async Task MaxMessageSize() - { - _systemClient.ReverseBytes(new byte[MaxReceivedMessageSizeInMegabytes * 1024 * 1024]).ShouldThrow(); - await Guid(); - } - - [Fact] - public async Task Guid() - { - var newGuid = System.Guid.NewGuid(); - var guid = await _systemClient.GetGuid(newGuid); - guid.ShouldBe(newGuid); - } - - [Fact] - public Task LargeMessage() => _systemClient.ReverseBytes(new byte[(int)(0.7 * MaxReceivedMessageSizeInMegabytes * 1024 * 1024)]); - - [Fact] - public async Task ReverseBytes() - { - var input = Encoding.UTF8.GetBytes("Test"); - var reversed = await _systemClient.ReverseBytes(input); - reversed.ShouldBe(input.Reverse()); - } - - [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 not configured."); - exception.Is().ShouldBeTrue(); - await Guid(); - } - - - [Fact] - public async Task VoidIsAsync() => await _systemClient.VoidSyncThrow(); - - [Fact] - public async Task GetThreadName() => (await _systemClient.GetThreadName()).ShouldBe("GuiThread"); - - [Fact] - public async Task Echo() - { - using var stream = await _systemClient.Echo(new MemoryStream(Encoding.UTF8.GetBytes("Hello world"))); - (await new StreamReader(stream).ReadToEndAsync()).ShouldBe("Hello world"); - } - - [Fact] - public async Task CancelUpload() - { - var stream = new MemoryStream(Enumerable.Range(1, 50000).Select(i=>(byte)i).ToArray()); - await _systemClient.GetThreadName(); - using (var cancellationSource = new CancellationTokenSource(5)) - { - _systemClient.Upload(stream, 20, cancellationSource.Token).ShouldThrow(); - } - } - - [Fact] - public async Task Upload() - { - (await _systemClient.Upload(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBe("Hello world"); - await Guid(); - } - - [Fact] - public virtual async Task UploadNoRead() - { - try - { - (await _systemClient.UploadNoRead(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBeEmpty(); - } - catch (IOException) { } - catch (ObjectDisposedException) { } - await Guid(); - } - - [Fact] - public Task DownloadUiThread() => Task.Factory.StartNew(Download, default, TaskCreationOptions.DenyChildAttach, GuiScheduler).Unwrap(); - [Fact] - public async Task Download() - { - 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(); - } - protected abstract TBuilder CreateSystemClientBuilder(); - protected TBuilder SystemClientBuilder() => CreateSystemClientBuilder().SerializeParametersAsObjects().RequestTimeout(RequestTimeout).Logger(_serviceProvider); - [Fact] - public async Task BeforeCall() - { - bool newConnection = false; - var proxy = SystemClientBuilder().BeforeCall(async (c, _) => - { - 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(); - } - - [Fact] - public async Task DontReconnect() - { - var proxy = SystemClientBuilder().DontReconnect().ValidateAndBuild(); - await proxy.GetGuid(System.Guid.Empty); - ((IpcProxy)proxy).CloseConnection(); - ObjectDisposedException exception = null; - try - { - await proxy.GetGuid(System.Guid.Empty); - } - catch (ObjectDisposedException ex) - { - exception = ex; - } - exception.ShouldNotBeNull(); - } - [Fact] - public Task CancelServerCall() => CancelServerCallCore(10); - protected ISystemService CreateSystemService() => SystemClientBuilder().ValidateAndBuild(); - - async Task CancelServerCallCore(int counter) - { - for (int i = 0; i < counter; i++) - { - 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); - } - } - [Fact] - public async Task ClosingTheHostShouldCloseTheConnection() - { - 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(); - } - [Fact] - public virtual async void BeforeCallServerSide() - { - var newGuid = System.Guid.NewGuid(); - MethodInfo method = null; - using var protectedService = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint(new EndpointSettings - { - 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))); - } -} \ No newline at end of file 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 deleted file mode 100644 index 98d75a4e..00000000 --- a/src/UiPath.CoreIpc.Tests/TestBase.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Nito.AsyncEx; - -namespace UiPath.CoreIpc.Tests; - -public abstract class TestBase : IDisposable -{ - protected const int MaxReceivedMessageSizeInMegabytes = 1; - protected static int Count = -1; - public static readonly TimeSpan RequestTimeout = -#if CI - TimeSpan.FromSeconds(2) + -#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() - { - _guiThread.SynchronizationContext.Send(() => Thread.CurrentThread.Name = "GuiThread"); - _serviceProvider = IpcHelpers.ConfigureServices(); - } - - protected static int GetCount() => Interlocked.Increment(ref Count); - - protected TaskScheduler GuiScheduler => _guiThread.Scheduler; - - public virtual void Dispose() => _guiThread.Dispose(); - protected virtual TSettings Configure(TSettings listenerSettings) where TSettings : ListenerSettings - { - listenerSettings.RequestTimeout = RequestTimeout; - listenerSettings.MaxReceivedMessageSizeInMegabytes = MaxReceivedMessageSizeInMegabytes; - return listenerSettings; - } - protected abstract ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj deleted file mode 100644 index eaf03da6..00000000 --- a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net6.0;net461;net6.0-windows - $(NoWarn);1998 - $(DefineConstants);$(DefineConstantsEx) - latest - true - - - - - - - - - - - - - - - - - - - - - - \ 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/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..dfe12f2b 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -1,138 +1,110 @@ -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 IServiceClientConfig 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) + var ct = timeoutHelper.Token; + + var (connection, newConnection) = await EnsureConnection(ct); + + if (Config.BeforeCall is not null) { - await _beforeCall(new(newConnection, method, args), token); + var callInfo = new CallInfo(newConnection, method, args); + await Config.BeforeCall(callInfo, ct); } - var requestId = _connection.NewRequestId(); - var request = new Request(typeof(TInterface).Name, requestId, methodName, serializedArguments, ObjectParameters ? args : null, messageTimeout.TotalSeconds) + + 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.DebugName); + + 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.DebugName); } - 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.DebugName, ex); + throw; } - return response.Deserialize(_serializer, ObjectParameters); + + return response.Deserialize(Config.Serializer); } 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 +122,167 @@ 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] = Config.Serializer.OrDefault().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.DebugName; + + #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 - { - if (clientConnection.Connected) - { - ReuseClientConnection(clientConnection); - return false; - } - 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) + get => _latestConnection; + set { - var sslStream = new SslStream(network); - try + if (_latestConnection == value) { - await sslStream.AuthenticateAsClientAsync(SslServer); + return; } - 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; + _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.Serializer, Config.Logger, Config.DebugName); + var router = new Router(_client.Config.CreateCallbackRouterConfig(), _client.Config.ServiceProvider); + _latestServer = new Server(router, _client.Config.RequestTimeout, LatestConnection); + + _ = Pal(); + return (LatestConnection, newlyConnected: true); + + async Task Pal() + { + try + { + await LatestConnection.Listen(); + } + catch (Exception ex) + { + Config.Logger.LogException(ex, Config.DebugName); + } + } } } - 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 IServiceClientConfig Config => _client.Config; } -public class IpcProxy : DispatchProxy, IDisposable +internal sealed class ServiceClientForCallback : ServiceClient { - private static readonly MethodInfo InvokeMethod = typeof(IpcProxy).GetStaticMethod(nameof(GenericInvoke)); - private static readonly ConcurrentDictionary InvokeByType = new(); - - internal IServiceClient ServiceClient { get; set; } - - public Connection Connection => ServiceClient.Connection; + private readonly Connection _connection; + private readonly Listener _listener; - protected override object Invoke(MethodInfo targetMethod, object[] args) => GetInvoke(targetMethod)(ServiceClient, targetMethod, args); + public override Stream? Network => _connection.Network; - public void Dispose() => ServiceClient.Dispose(); - - public void CloseConnection() => Connection?.Dispose(); + public ServiceClientForCallback(Connection connection, Listener listener, Type interfaceType) : base(interfaceType) + { + _connection = connection; + _listener = listener; + } - 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 IServiceClientConfig Config => _listener.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/ClientConfig.cs b/src/UiPath.CoreIpc/Config/ClientConfig.cs new file mode 100644 index 00000000..6b0191ac --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ClientConfig.cs @@ -0,0 +1,60 @@ +using System.ComponentModel; + +namespace UiPath.Ipc; + +public sealed record ClientConfig : EndpointConfig, IServiceClientConfig +{ + public EndpointCollection? Callbacks { get; init; } + + public IServiceProvider? ServiceProvider { get; init; } + public ILogger? Logger { get; init; } + public BeforeConnectHandler? BeforeConnect { get; init; } + public BeforeCallHandler? BeforeCall { get; init; } + public TaskScheduler? Scheduler { get; init; } + public ISerializer? Serializer { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public string DebugName { get; set; } = null!; + + internal void Validate() + { + var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; + + if (haveDeferredInjectedCallbacks && ServiceProvider is null) + { + throw new InvalidOperationException("ServiceProvider is required when you register injectable callbacks. Consider registering a callback instance."); + } + } + + internal ILogger? GetLogger(string name) + { + if (Logger is not null) + { + return Logger; + } + + if (ServiceProvider?.GetService() is not { } loggerFactory) + { + return null; + } + + return loggerFactory.CreateLogger(name); + } + + internal override RouterConfig CreateCallbackRouterConfig() + => RouterConfig.From( + Callbacks.OrDefault(), + endpoint => endpoint with + { + BeforeCall = null, // callbacks don't support BeforeCall + Scheduler = endpoint.Scheduler ?? Scheduler + }); +} + +public interface IClientState : IDisposable +{ + Stream? Network { get; } + + bool IsConnected(); + ValueTask Connect(IpcClient client, CancellationToken ct); +} \ 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..2e21728a --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ClientTransport.cs @@ -0,0 +1,7 @@ +namespace UiPath.Ipc; + +public abstract record ClientTransport +{ + public abstract IClientState CreateState(); + public abstract void Validate(); +} diff --git a/src/UiPath.CoreIpc/Config/EndpointCollection.cs b/src/UiPath.CoreIpc/Config/EndpointCollection.cs new file mode 100644 index 00000000..8174056f --- /dev/null +++ b/src/UiPath.CoreIpc/Config/EndpointCollection.cs @@ -0,0 +1,21 @@ +using System.Collections; + +namespace UiPath.Ipc; + +public class EndpointCollection : IEnumerable, IEnumerable +{ + internal readonly Dictionary Endpoints = new(); + + public void Add(Type type) => Add(type, instance: null); + public void Add(Type contractType, object? instance) => Add(new EndpointSettings(contractType, instance)); + public void Add(EndpointSettings endpointSettings) + { + if (endpointSettings is null) throw new ArgumentNullException(nameof(endpointSettings)); + // endpointSettings.Validate(); + + Endpoints[endpointSettings.Service.Type] = endpointSettings; + } + public IEnumerator GetEnumerator() => Endpoints.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/UiPath.CoreIpc/Config/EndpointConfig.cs b/src/UiPath.CoreIpc/Config/EndpointConfig.cs new file mode 100644 index 00000000..4a06504a --- /dev/null +++ b/src/UiPath.CoreIpc/Config/EndpointConfig.cs @@ -0,0 +1,10 @@ +namespace UiPath.Ipc; + +public abstract record EndpointConfig +{ + public TimeSpan RequestTimeout { get; init; } = Timeout.InfiniteTimeSpan; + + internal virtual RouterConfig CreateRouterConfig(IpcServer server) => throw new NotSupportedException(); + + internal virtual RouterConfig CreateCallbackRouterConfig() => throw new NotSupportedException(); +} diff --git a/src/UiPath.CoreIpc/Config/IListenerConfig.cs b/src/UiPath.CoreIpc/Config/IListenerConfig.cs new file mode 100644 index 00000000..a9513957 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IListenerConfig.cs @@ -0,0 +1,12 @@ +namespace UiPath.Ipc.Extensibility; + +public interface IListenerConfig + where TSelf : ListenerConfig, IListenerConfig + where TListenerState : IAsyncDisposable +{ + TListenerState CreateListenerState(IpcServer server); + TConnectionState CreateConnectionState(IpcServer server, TListenerState listenerState); + ValueTask AwaitConnection(TListenerState listenerState, TConnectionState connectionState, CancellationToken ct); + IEnumerable Validate(); +} + diff --git a/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs b/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs new file mode 100644 index 00000000..95d3a29e --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs @@ -0,0 +1,11 @@ +namespace UiPath.Ipc; + +internal interface IServiceClientConfig +{ + TimeSpan RequestTimeout { get; } + BeforeConnectHandler? BeforeConnect { get; } + BeforeCallHandler? BeforeCall { get; } + ILogger? Logger { get; } + ISerializer? Serializer { get; } + string DebugName { get; } +} diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs new file mode 100644 index 00000000..d09c7e08 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -0,0 +1,35 @@ +namespace UiPath.Ipc; + +public sealed class IpcClient +{ + private ClientTransport _transport = null!; + + public required ClientConfig Config { get; init; } + public required ClientTransport Transport { get; init; } + + 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 void Validate() + { + if (Config is null) + { + throw new InvalidOperationException($"{Config} is required."); + } + if (Transport is null) + { + throw new InvalidOperationException($"{Transport} is required."); + } + + Config.Validate(); + Transport.Validate(); + + Config.DebugName ??= Transport.ToString(); + } +} diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs new file mode 100644 index 00000000..d4347d96 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -0,0 +1,96 @@ +namespace UiPath.Ipc; + +public sealed class IpcServer : IAsyncDisposable +{ + public required IServiceProvider ServiceProvider { get; init; } + public required EndpointCollection Endpoints { get; init; } + public required IReadOnlyList Listeners { get; init; } + public TaskScheduler? Scheduler { get; init; } + + private readonly Lazy> _started; + private readonly TaskCompletionSource _tcsStopped = new(); + + public IpcServer() => _started = new(StartCore); + + public void Start() + { + if (!IsValid(out var errors)) + { + throw new InvalidOperationException($"ValidationErrors:\r\n{string.Join("\r\n", errors)}"); + } + + _ = _started.Value; + } + public Task WaitForStart() => _started.Value; + public Task WaitForStop() => _tcsStopped.Task; + + private async Task StartCore() + { + if (!IsValid(out _)) + { + return null; + } + + var disposables = new StopAdapter(this); + try + { + foreach (var listenerConfig in Listeners) + { + disposables.Add(Listener.Create(this, listenerConfig)); + } + return disposables; + } + catch (Exception ex) + { + Trace.TraceError($"Failed to start server. Ex: {ex}"); + disposables.SetException(ex); + await disposables.DisposeAsync(); + throw; + } + } + + private bool IsValid(out IReadOnlyList errors) + { + errors = Listeners.SelectMany(PrefixErrors).ToArray(); + return errors is { Count: 0 }; + + static IEnumerable PrefixErrors(ListenerConfig config) + => config.Validate().Select(error => $"{config.GetType().Name}: {error}"); + } + + public async ValueTask DisposeAsync() + { + var maybeLogger = ServiceProvider.GetService()?.CreateLogger(typeof(IpcServer)); + + await ((await _started.Value)?.DisposeAsync() ?? default); + } + + private sealed class StopAdapter : IAsyncDisposable + { + private readonly List _items = new(); + private readonly IpcServer _server; + private Exception? _exception; + + public StopAdapter(IpcServer server) => _server = server; + + public void Add(IAsyncDisposable item) => _items.Add(item); + + public void SetException(Exception ex) => _exception = ex; + + public async ValueTask DisposeAsync() + { + foreach (var item in _items) + { + await item.DisposeAsync(); + } + + if (_exception is not null) + { + _server._tcsStopped.TrySetException(_exception); + return; + } + + _server._tcsStopped.TrySetResult(null); + } + } +} diff --git a/src/UiPath.CoreIpc/Config/ListenerConfig.cs b/src/UiPath.CoreIpc/Config/ListenerConfig.cs new file mode 100644 index 00000000..2c41dcc6 --- /dev/null +++ b/src/UiPath.CoreIpc/Config/ListenerConfig.cs @@ -0,0 +1,31 @@ +using System.Security.Cryptography.X509Certificates; + +namespace UiPath.Ipc; + +public abstract record ListenerConfig : EndpointConfig, IServiceClientConfig +{ + public int ConcurrentAccepts { get; init; } = 5; + public byte MaxReceivedMessageSizeInMegabytes { get; init; } = 2; + public X509Certificate? Certificate { get; init; } + internal int MaxMessageSize => MaxReceivedMessageSizeInMegabytes * 1024 * 1024; + + internal IEnumerable Validate() => Enumerable.Empty(); + + internal override RouterConfig CreateRouterConfig(IpcServer server) + => RouterConfig.From( + server.Endpoints, + endpoint => endpoint with + { + Scheduler = endpoint.Scheduler ?? server.Scheduler + }); + + #region IServiceClientConfig + /// Do not implement explicitly, as it must be implicitly implemented by . + + BeforeConnectHandler? IServiceClientConfig.BeforeConnect => null; + BeforeCallHandler? IServiceClientConfig.BeforeCall => null; + ILogger? IServiceClientConfig.Logger => null; + ISerializer? IServiceClientConfig.Serializer => null!; + string IServiceClientConfig.DebugName => $"CallbackClient for {this}"; + #endregion +} diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index e4404147..a8c3aa30 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -1,7 +1,13 @@ -namespace UiPath.CoreIpc; +using Newtonsoft.Json; +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,34 +18,40 @@ 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 Stream Network { get; } + public ILogger? Logger { get; internal set; } + + [MemberNotNullWhen(returnValue: true, nameof(Logger))] + public bool LogEnabled => Logger.Enabled(); + + public string DebugName { get; } + public ISerializer? Serializer { get; } + + public Connection(Stream network, ISerializer? serializer, ILogger? logger, string debugName, int maxMessageSize = int.MaxValue) { Network = network; _nestedStream = new NestedStream(network, 0); Serializer = serializer; Logger = logger; - Name = $"{name} {GetHashCode()}"; + DebugName = $"{debugName} {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 Task Listen() => _receiveLoop.Value; + internal event Func RequestReceived; internal event Action CancellationReceived; - public event EventHandler Closed; + public event EventHandler? Closed; #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] #endif @@ -51,10 +63,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?.LogError($"Caught exception while sending the request. Ex: {ex}"); tokenRegistration.Dispose(); if (_requests.TryRemove(requestId, out _)) { @@ -64,7 +79,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 { @@ -75,7 +102,7 @@ internal async ValueTask RemoteCall(Request request, CancellationToken } internal 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 ? @@ -95,7 +122,6 @@ Task CancelServerCall(string requestId) => } internal 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 +146,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 +163,17 @@ 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 +213,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 +255,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 +265,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 +290,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 +307,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 +328,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 +351,7 @@ private MemoryStream SerializeToStream(object value) try { stream.Position = HeaderLength; - Serializer.Serialize(value, stream); + Serializer.OrDefault().Serialize(value, stream); return stream; } catch @@ -312,30 +360,34 @@ private MemoryStream SerializeToStream(object value) throw; } } - private ValueTask Deserialize() => Serializer.DeserializeAsync(_nestedStream); - private void OnCancellationReceived(string requestId) + private ValueTask Deserialize() => Serializer.OrDefault().DeserializeAsync(_nestedStream, Logger); + + private void OnCancellationReceived(CancellationRequest cancellationRequest) { try { - CancellationReceived(requestId); + CancellationReceived(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(request); } catch (Exception ex) { Log(ex); } - return default; } private void OnResponseReceived(Response response) { @@ -343,7 +395,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 +407,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..ab295693 --- /dev/null +++ b/src/UiPath.CoreIpc/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using UiPath.Ipc.Extensibility; +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..9752811d --- /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 ISerializer OrDefault(this ISerializer? serializer) => serializer ?? IpcJsonSerializer.Instance; + public static ILoggerFactory OrDefault(this ILoggerFactory? loggerFactory) => loggerFactory ?? NullLoggerFactory.Instance; + 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 EndpointCollection OrDefault(this EndpointCollection? 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..abbfedf9 100644 --- a/src/UiPath.CoreIpc/Helpers.cs +++ b/src/UiPath.CoreIpc/Helpers/Helpers.cs @@ -1,31 +1,24 @@ using Microsoft.IO; using System.Collections.ObjectModel; 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 +30,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 +44,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 +92,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()); @@ -120,7 +131,7 @@ public static PipeSecurity AllowCurrentUser(this PipeSecurity pipeSecurity, bool { return pipeSecurity; } - pipeSecurity.Allow(currentIdentity.User, PipeAccessRights.ReadWrite|PipeAccessRights.CreateNewInstance); + pipeSecurity.Allow(currentIdentity.User, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance); } return pipeSecurity; } @@ -161,26 +172,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 +277,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 98% rename from src/UiPath.CoreIpc/NestedStream.cs rename to src/UiPath.CoreIpc/Helpers/NestedStream.cs index fdc40877..2e69166e 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. diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs new file mode 100644 index 00000000..ff3d25a6 --- /dev/null +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -0,0 +1,140 @@ +namespace UiPath.Ipc; + +internal readonly record struct RouterConfig(IReadOnlyDictionary Endpoints) +{ + public static RouterConfig From(EndpointCollection 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(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, EndpointSettings endpointSettings) + => new Route() + { + Service = endpointSettings.Service.WithProvider(serviceProvider), + BeforeCall = endpointSettings.BeforeCall, + Scheduler = endpointSettings.Scheduler.OrDefault(), + LoggerFactory = serviceProvider.MaybeCreateServiceFactory(), + Serializer = serviceProvider.MaybeCreateServiceFactory() + }; + + public required ServiceFactory Service { get; init; } + + public TaskScheduler Scheduler { get; init; } + public BeforeCallHandler? BeforeCall { get; init; } + public Func? LoggerFactory { get; init; } + public Func? Serializer { get; init; } +} diff --git a/src/UiPath.CoreIpc/TaskCompletionPool.cs b/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs similarity index 98% rename from src/UiPath.CoreIpc/TaskCompletionPool.cs rename to src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs index 11f9ece9..f1f971a2 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(); 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..7de71a60 --- /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.Information, + 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.Information, + 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.Error, + 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.Information, + 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.Information, + 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..0032e3dd --- /dev/null +++ b/src/UiPath.CoreIpc/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/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/CollectionExtensions.cs b/src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs new file mode 100644 index 00000000..c850d4a9 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs @@ -0,0 +1,19 @@ +#if NETFRAMEWORK + +namespace System.Collections.Generic; + +public static class CollectionExtensions +{ + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + + dictionary.Add(key, value); + return true; + } +} + +#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..48bf84b1 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +#if NET461 + +namespace System.Linq; + +public 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/MemberNotNullWhenAttribute.cs b/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs new file mode 100644 index 00000000..bf9ad270 --- /dev/null +++ b/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.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/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/EndpointSettings.cs b/src/UiPath.CoreIpc/Server/EndpointSettings.cs new file mode 100644 index 00000000..26a39d6b --- /dev/null +++ b/src/UiPath.CoreIpc/Server/EndpointSettings.cs @@ -0,0 +1,58 @@ +namespace UiPath.Ipc; + +using System; + +public record EndpointSettings +{ + public TaskScheduler? Scheduler { get; set; } + public BeforeCallHandler? BeforeCall { get; set; } + public Type ContractType => Service.Type; + public object? ServiceInstance => Service.MaybeGetInstance(); + public IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider(); + internal ServiceFactory Service { get; } + + public EndpointSettings(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 EndpointSettings(Type contractType, IServiceProvider serviceProvider) : this( + new ServiceFactory.Injected() + { + Type = contractType ?? throw new ArgumentNullException(nameof(contractType)), + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)) + }) + { } + + private protected EndpointSettings(ServiceFactory service) => Service = service; + + public virtual EndpointSettings WithServiceProvider(IServiceProvider? serviceProvider) + => new(Service.WithProvider(serviceProvider)); + + public void Validate() + { + Validator.Validate(Service.Type); + if (Service.MaybeGetInstance() is { } instance && !Service.Type.IsAssignableFrom(instance.GetType())) + { + throw new ArgumentOutOfRangeException(nameof(instance)); + } + } +} + +public sealed record EndpointSettings : EndpointSettings where TContract : class +{ + public EndpointSettings(TContract? serviceInstance = null) : base(typeof(TContract), serviceInstance) { } + public EndpointSettings(IServiceProvider serviceProvider) : base(typeof(TContract), serviceProvider) { } + private EndpointSettings(ServiceFactory service) : base(service) { } + + public override EndpointSettings WithServiceProvider(IServiceProvider? serviceProvider) + => new EndpointSettings(Service.WithProvider(serviceProvider)); +} diff --git a/src/UiPath.CoreIpc/Server/Listener.cs b/src/UiPath.CoreIpc/Server/Listener.cs index a9c10adf..9f999d69 100644 --- a/src/UiPath.CoreIpc/Server/Listener.cs +++ b/src/UiPath.CoreIpc/Server/Listener.cs @@ -1,80 +1,226 @@ -using System.Security.Cryptography.X509Certificates; +using System.Linq.Expressions; -namespace UiPath.CoreIpc; +namespace UiPath.Ipc; -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 +using ListenerFactory = Func; + +internal abstract class Listener : IAsyncDisposable { - protected Listener(ListenerSettings settings) + public static Listener Create(IpcServer server, ListenerConfig config) + => GetFactory(config.GetType())( + server ?? throw new ArgumentNullException(nameof(server)), + config ?? throw new ArgumentNullException(nameof(config))); + + private readonly struct Result { - Settings = settings; - MaxMessageSize = settings.MaxReceivedMessageSizeInMegabytes * 1024 * 1024; + 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; + } } - 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) + private static readonly ConcurrentDictionary> Factories = new(); + private static ListenerFactory GetFactory(Type configType) => Factories.GetOrAdd(configType, CreateFactory).Value; + private static Result CreateFactory(Type configType) { - Logger = ServiceProvider.GetRequiredService().CreateLogger(GetType()); - if (LogEnabled) + try { - Log($"Starting listener {Name}..."); + return new(Pal(configType)); } - return Task.WhenAll(Enumerable.Range(1, Settings.ConcurrentAccepts).Select(async _ => + catch (Exception ex) + { + return new(ex); + } + + static ListenerFactory Pal(Type configType) { - while (!token.IsCancellationRequested) + if (configType.GetInterfaces().SingleOrDefault(IsIListenerConfig) is not { } iface + || iface.GetGenericArguments() is not [_, var listenerStateType, var connectionStateType]) { - await AcceptConnection(token); + throw new ArgumentOutOfRangeException(nameof(iface), $"The ListenerConfig type must implement IListenerConfig<,>. ListenerConfig type was: {configType.FullName}"); } - })); + + var listenerType = typeof(Listener<,,>).MakeGenericType(configType, listenerStateType, connectionStateType); + var listenerCtor = listenerType.GetConstructor( + bindingAttr: BindingFlags.Public | BindingFlags.Instance, + binder: Type.DefaultBinder, + types: [typeof(IpcServer), configType], + modifiers: null)!; + + var paramofServer = Expression.Parameter(typeof(IpcServer)); + var paramofConfig = Expression.Parameter(typeof(ListenerConfig)); + var lambda = Expression.Lambda( + delegateType: typeof(ListenerFactory), + body: Expression.New(listenerCtor, paramofServer, Expression.Convert(paramofConfig, configType)), + paramofServer, + paramofConfig); + + var @delegate = (lambda.Compile() as ListenerFactory)!; + return @delegate; + } + + static bool IsIListenerConfig(Type candidateIface) + => candidateIface.IsGenericType && candidateIface.GetGenericTypeDefinition() == typeof(IListenerConfig<,,>); + } + + private readonly Lazy _disposeTask = null!; + public readonly ListenerConfig Config; + public readonly IpcServer Server; + private readonly Lazy _loggerCategory; + + private readonly Lazy _lazyLogger; + public ILogger Logger => _lazyLogger.Value; + + protected Listener(IpcServer server, ListenerConfig config) + { + _loggerCategory = new(ComputeLoggerCategory); + Config = config; + Server = server; + _lazyLogger = new(() => server.ServiceProvider.GetService().OrDefault().CreateLogger(LoggerCategory)); + _disposeTask = new(DisposeCore); + } + + ValueTask IAsyncDisposable.DisposeAsync() => new(_disposeTask.Value); + + protected abstract Task DisposeCore(); + + private string LoggerCategory => _loggerCategory.Value; + + private string ComputeLoggerCategory() + => $"{GetType().Namespace}.{nameof(Listener)}<{ConfigType.Name}[{Config}],..>"; + + protected abstract Type ConfigType { get; } +} + +internal sealed class Listener : Listener, IAsyncDisposable + where TConfig : ListenerConfig, IListenerConfig + where TListenerState : IAsyncDisposable + where TConnectionState : IDisposable +{ + private readonly CancellationTokenSource _cts = new(); + private readonly Task _listeningTask = null!; + + public new readonly TConfig Config; + + public TListenerState State { get; } + + public Listener(IpcServer server, TConfig config) : base(server, config) + { + Config = config; + State = Config.CreateListenerState(server); + + _listeningTask = Task.Run(() => Listen(_cts.Token)); + } + + public void Log(string message) + { + if (!Logger.Enabled()) + { + return; + } + + Logger.LogInformation(message); } - protected abstract ServerConnection CreateServerConnection(); - async Task AcceptConnection(CancellationToken token) + public void LogError(Exception exception, string message) { - var serverConnection = CreateServerConnection(); + if (!Logger.Enabled(LogLevel.Error)) + { + return; + } + + Logger.LogError(exception, message); + } + + protected override async Task DisposeCore() + { + Log($"Stopping listener {Config}..."); try { - var network = await serverConnection.AcceptClient(token); - serverConnection.Listen(network, token).LogException(Logger, Name); + _cts.Cancel(); } catch (Exception ex) { - serverConnection.Dispose(); - if (!token.IsCancellationRequested) + LogError(ex, $"Canceling {Config} failed."); + } + try + { + await _listeningTask; + } + catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token) + { + Log($"Stopping listener {Config} threw OCE."); + } + catch (Exception ex) + { + LogError(ex, $"Stopping listener {Config} failed."); + } + await State.DisposeAsync(); + _cts.Dispose(); + } + + private async Task Listen(CancellationToken ct) + { + Log($"Starting listener {Config}..."); + + await Task.WhenAll(Enumerable.Range(1, Config.ConcurrentAccepts).Select(async _ => + { + while (!ct.IsCancellationRequested) { - Logger.LogException(ex, Settings.Name); + await AcceptConnection(ct); } - } + })); } - protected virtual void Dispose(bool disposing) + private async Task AcceptConnection(CancellationToken ct) { - if (!disposing) + var serverConnection = new ServerConnection(this); + + Stream? network = null; + try + { + network = await serverConnection.AcceptClient(ct); + } + catch { + serverConnection.Dispose(); return; } - Settings.Certificate?.Dispose(); - } - public void Dispose() - { - if (LogEnabled) + + try + { + _ = Task.Run(TryToListen); + } + catch (Exception ex) { - Log($"Stopping listener {Name}..."); + serverConnection.Dispose(); + if (!ct.IsCancellationRequested) + { + Logger.LogException(ex, Config); + } + } + + async Task TryToListen() + { + try + { + await serverConnection.Listen(network, ct); + } + catch (Exception ex) + { + Logger.LogException(ex, $"Listen loop failed for {Config}"); + } } - 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 + + protected override Type ConfigType => typeof(TConfig); +} diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index 5e6d5b4d..e54c6c96 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -1,20 +1,40 @@ 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 => Logger.Enabled(); + public ISerializer Serializer => _connection.Serializer.OrDefault(); + 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 +43,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 +53,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 +67,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 +112,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 +121,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, Serializer.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 +213,21 @@ void Deserialize() } else { - argument = objectParameters ? - Serializer.Deserialize(request.ObjectParameters[index], parameterType) : - Serializer.Deserialize(request.Parameters[index], parameterType); + argument = Serializer.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; } return argument; } @@ -222,46 +240,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 +298,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..84aafe9e 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -1,85 +1,155 @@ -using System.Net.Security; -namespace UiPath.CoreIpc; +using System.IO.Pipes; +using System.Net.Security; +namespace UiPath.Ipc; public interface IClient { - TCallbackInterface GetCallback(Type callbackContract, bool objectParameters) where TCallbackInterface : class; + TCallbackInterface GetCallback() where TCallbackInterface : class; void Impersonate(Action action); } -abstract class ServerConnection : IClient, IDisposable + +internal sealed class ServerConnection : ServerConnection + where TConfig : ListenerConfig, IListenerConfig + where TListenerState : IAsyncDisposable + where TConnectionState : IDisposable { - 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 new readonly Listener Listener; + + private readonly object _lock = new(); + private bool _acceptClientCalled = false; + private bool _disposed = false; + private TConnectionState? _connectionState; + + public ServerConnection(Listener listener) : base(listener) => Listener = listener; + + public override ValueTask AcceptClient(CancellationToken ct) { - if (callbackContract == null) + lock (_lock) { - throw new InvalidOperationException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's not configured."); + if (_disposed) + { + throw new ObjectDisposedException(nameof(ServerConnection)); + } + if (_acceptClientCalled) + { + throw new InvalidOperationException("AcceptClient can only be called once."); + } + _acceptClientCalled = true; + _connectionState = Listener.Config.CreateConnectionState(Listener.Server, Listener.State); } - return (TCallbackInterface)_callbacks.GetOrAdd(callbackContract, CreateCallback); - TCallbackInterface CreateCallback(Type callbackContract) + + return Listener.Config.AwaitConnection(Listener.State, _connectionState, ct); + } + + public override void Dispose() + { + base.Dispose(); + + lock (_lock) { - if (!typeof(TCallbackInterface).IsAssignableFrom(callbackContract)) + if (_disposed) { - throw new ArgumentException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's {callbackContract}."); + return; } - if (_listener.LogEnabled) + _disposed = true; + + if (_connectionState is not null) { - _listener.Log($"Create callback {callbackContract} {_listener.Name}"); + _connectionState.Dispose(); } - _connectionAsTask ??= Task.FromResult(_connection); - var serviceClient = new ServiceClient(_connection.Serializer, Settings.RequestTimeout, Logger, (_, _) => _connectionAsTask) - { - ObjectParameters = objectParameters - }; - return serviceClient.CreateProxy(); + } + } +} + +internal abstract class ServerConnection : IClient, IDisposable +{ + public readonly Listener Listener; + + private readonly ConcurrentDictionary _callbacks = new(); + internal Connection? Connection; + private Task? _connectionAsTask; + private Server? Server; + + protected ServerConnection(Listener listener) => Listener = listener; + + protected internal virtual void Initialize() { } + + public abstract ValueTask AcceptClient(CancellationToken cancellationToken); + + public void Impersonate(Action action) + { + if (Connection is null) + { + throw new InvalidOperationException("The server connection is not listening yet."); + } + + if (Connection.Network is not NamedPipeServerStream pipeStream) + { + action(); + return; + } + + pipeStream.RunAsClient(() => action()); + } + + TCallbackInterface IClient.GetCallback() where TCallbackInterface : class + { + return (TCallbackInterface)_callbacks.GetOrAdd(typeof(TCallbackInterface), CreateCallback); + + TCallbackInterface CreateCallback(Type callbackContract) + { + Listener.Logger.LogInformation($"Create callback {callbackContract} {Listener.Config}"); + + _connectionAsTask ??= Task.FromResult(Connection!); + + // TODO: rethink this double specification of TCallbackInterface + return new ServiceClientForCallback(Connection!, Listener, typeof(TCallbackInterface)).GetProxy(); } } public async Task Listen(Stream network, CancellationToken cancellationToken) { - var stream = await AuthenticateAsServer(); - var serializer = Settings.ServiceProvider.GetRequiredService(); - _connection = new(stream, serializer, Logger, _listener.Name, _listener.MaxMessageSize); - _server = new(Settings, _connection, this); + var stream = await AuthenticateAsServer(); // TODO: should we decommission this? + var serializer = Listener.Server.ServiceProvider.GetService(); + + Connection = new Connection(stream, serializer, Listener.Logger, debugName: Listener.ToString()!, maxMessageSize: Listener.Config.MaxMessageSize); + + Server = new Server( + new Router( + Listener.Config.CreateRouterConfig(Listener.Server), + Listener.Server.ServiceProvider), + Listener.Config.RequestTimeout, + Connection, + client: this); + // close the connection when the service host closes - using (cancellationToken.UnsafeRegister(state => ((Connection)state).Dispose(), _connection)) + using (cancellationToken.UnsafeRegister(_ => Connection.Dispose(), state: null)) { - await _connection.Listen(); + await Connection.Listen(); } return; + + // TODO: should we decommission this? async Task AuthenticateAsServer() { - var certificate = Settings.Certificate; - if (certificate == null) + if (Listener.Config.Certificate is null) { return network; } + var sslStream = new SslStream(network); try { - await sslStream.AuthenticateAsServerAsync(certificate); + await sslStream.AuthenticateAsServerAsync(Listener.Config.Certificate); } catch { sslStream.Dispose(); throw; } + Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); return sslStream; } } - protected virtual void Dispose(bool disposing){} - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} \ No newline at end of file + public virtual void Dispose() { } +} 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/NamedPipeListener.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs new file mode 100644 index 00000000..4e687558 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using System.IO.Pipes; +using System.Security.Principal; + +namespace UiPath.Ipc.Transport.NamedPipe; + +using INamedPipeListenerConfig = IListenerConfig; + +public sealed record NamedPipeListener : ListenerConfig, INamedPipeListenerConfig +{ + public required string PipeName { get; init; } + public string ServerName { get; init; } = "."; + [JsonIgnore] + public AccessControlDelegate? AccessControl { get; init; } + + private PipeSecurity? GetPipeSecurity() + { + var setAccessControl = AccessControl; + if (setAccessControl is 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); + } + + NamedPipeListenerState INamedPipeListenerConfig.CreateListenerState(IpcServer server) + => new(); + + NamedPipeServerConnectionState INamedPipeListenerConfig.CreateConnectionState(IpcServer server, NamedPipeListenerState listenerState) + => new() + { + Stream = IOHelpers.NewNamedPipeServerStream( + PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + GetPipeSecurity) + }; + + async ValueTask INamedPipeListenerConfig.AwaitConnection(NamedPipeListenerState listenerState, NamedPipeServerConnectionState connectionState, CancellationToken ct) + { + await connectionState.Stream.WaitForConnectionAsync(ct); + return connectionState.Stream; + } + + IEnumerable INamedPipeListenerConfig.Validate() + { + if (PipeName is null or "") { yield return "PipeName is required"; } + } + + public override string ToString() => $"ServerPipe={PipeName}"; +} + +internal sealed class NamedPipeServerConnectionState : IDisposable +{ + public required NamedPipeServerStream Stream { get; init; } + + public void Dispose() => Stream.Dispose(); +} + +internal sealed class NamedPipeListenerState : IAsyncDisposable +{ + public ValueTask DisposeAsync() => default; +} diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs new file mode 100644 index 00000000..ba8fe11e --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs @@ -0,0 +1,51 @@ +using System.IO.Pipes; +using System.Security.Principal; + +namespace UiPath.Ipc.Transport.NamedPipe; + +public sealed record NamedPipeTransport : ClientTransport +{ + public required string PipeName { get; init; } + public string ServerName { get; init; } = "."; + public bool AllowImpersonation { get; init; } + + public override string ToString() => $"ClientPipe={PipeName}"; + + public override IClientState CreateState() => new NamedPipeClientState(); + + public 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 NamedPipeTransport ?? 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/Tcp/TcpListener.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs new file mode 100644 index 00000000..d9de0eaf --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Sockets; + +namespace UiPath.Ipc.Transport.Tcp; + +using ITcpListenerConfig = IListenerConfig; + +public sealed record TcpListener : ListenerConfig, ITcpListenerConfig +{ + public required IPEndPoint EndPoint { get; init; } + + TcpListenerState ITcpListenerConfig.CreateListenerState(IpcServer server) + { + var listener = new System.Net.Sockets.TcpListener(EndPoint); + listener.Start(backlog: ConcurrentAccepts); + + return new() { Listener = listener }; + } + + TcpServerConnectionState ITcpListenerConfig.CreateConnectionState(IpcServer server, TcpListenerState listenerState) + => new(); + + async ValueTask ITcpListenerConfig.AwaitConnection(TcpListenerState listenerState, TcpServerConnectionState connectionState, CancellationToken ct) + { + System.Net.Sockets.TcpClient tcpClient; +#if NET461 + using var ctreg = ct.Register(listenerState.Listener.Stop); + tcpClient = await listenerState.Listener.AcceptTcpClientAsync(); +#else + tcpClient = await listenerState.Listener.AcceptTcpClientAsync(ct); +#endif + return tcpClient.GetStream(); + } + + IEnumerable ITcpListenerConfig.Validate() + { + if (EndPoint is null) + { + yield return "EndPoint is required"; + } + } + + public override string ToString() => $"TcpServer={EndPoint}"; +} + +internal sealed class TcpListenerState : IAsyncDisposable +{ + public required System.Net.Sockets.TcpListener Listener { get; init; } + + public ValueTask DisposeAsync() + { + Listener.Stop(); + return default; + } +} + +internal sealed class TcpServerConnectionState : IDisposable +{ + public void Dispose() { } +} + diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs new file mode 100644 index 00000000..c114f4f6 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs @@ -0,0 +1,56 @@ +using System.Net; + +namespace UiPath.Ipc.Transport.Tcp; + +public sealed record TcpTransport : ClientTransport +{ + public required IPEndPoint EndPoint { get; init; } + + public override string ToString() => $"TcpClient={EndPoint}"; + + public override IClientState CreateState() => new TcpClientState(); + + public 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 TcpTransport ?? 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/WebSocket/WebSocketListener.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs new file mode 100644 index 00000000..95911151 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs @@ -0,0 +1,38 @@ +namespace UiPath.Ipc.Transport.WebSocket; + +using IWebSocketListenerConfig = IListenerConfig; + +public sealed record WebSocketListener : ListenerConfig, IWebSocketListenerConfig +{ + public required Accept Accept { get; init; } + + WebSocketListenerState IWebSocketListenerConfig.CreateListenerState(IpcServer server) + => new(); + + WebSocketServerConnectionState IWebSocketListenerConfig.CreateConnectionState(IpcServer server, WebSocketListenerState listenerState) + => new(); + + async ValueTask IWebSocketListenerConfig.AwaitConnection(WebSocketListenerState listenerState, WebSocketServerConnectionState connectionState, CancellationToken ct) + { + var webSocket = await Accept(ct); + + return new WebSocketStream(webSocket); + } + + IEnumerable IWebSocketListenerConfig.Validate() + { + if (Accept is null) { yield return "Accept is required"; } + } + + public override string ToString() => "WebSocketServer"; +} + +internal sealed class WebSocketListenerState : IAsyncDisposable +{ + public ValueTask DisposeAsync() => default; +} + +internal sealed class WebSocketServerConnectionState : IDisposable +{ + public void Dispose() { } +} diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs similarity index 93% rename from src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs rename to src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs index c64b4816..eac709f9 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 . +/// 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/Transport/WebSocket/WebSocketTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs new file mode 100644 index 00000000..6558b2c7 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs @@ -0,0 +1,39 @@ +using System.Net.WebSockets; + +namespace UiPath.Ipc.Transport.WebSocket; + +public sealed record WebSocketTransport : ClientTransport +{ + public required Uri Uri { get; init; } + public override string ToString() => $"WebSocketClient={Uri}"; + + public override IClientState CreateState() => new WebSocketClientState(); + + public 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 WebSocketTransport ?? 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/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index b0111764..af37d5f6 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,37 @@ true snupkg CA1416 - latest + preview true + enable + true + - - <_Parameter1>UiPath.CoreIpc.Tests - - + + + + + + + + + + + + + @@ -41,4 +55,9 @@ + + + + + \ No newline at end of file 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 59% rename from src/UiPath.CoreIpc/Dtos.cs rename to src/UiPath.CoreIpc/Wire/Dtos.cs index cbf278ba..a5cb95e9 100644 --- a/src/UiPath.CoreIpc/Dtos.cs +++ b/src/UiPath.CoreIpc/Wire/Dtos.cs @@ -1,16 +1,17 @@ -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; } [JsonIgnore] public TimeSpan RequestTimeout { get; set; } public TCallbackInterface GetCallback() where TCallbackInterface : class => - Client.GetCallback(CallbackContract, ObjectParameters); + Client.GetCallback(); public void ImpersonateClient(Action action) => Client.Impersonate(action); } public class Message : Message @@ -18,34 +19,50 @@ 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(ISerializer? serializer) + { if (Error != null) { throw new RemoteException(Error); } - return (TResult)(DownloadStream ?? (objectParameters ? - serializer.Deserialize(ObjectData, typeof(TResult)) : serializer.Deserialize(Data ?? "", typeof(TResult)))); + + return (TResult)(DownloadStream ?? serializer.OrDefault().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 @@ -57,7 +74,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 +98,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..037e9fc1 --- /dev/null +++ b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs @@ -0,0 +1,17 @@ +namespace UiPath.Ipc; + +[Serializable] +public sealed class EndpointNotFoundException : ArgumentOutOfRangeException +{ + public string ServerDebugName { get; } + public string EndpointName { get; } + + public EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) + : base(paramName, FormatMessage(serverDebugName, endpointName)) + { + 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..ff78f64a --- /dev/null +++ b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; +using System.Buffers; +using System.Globalization; +using System.Text; + +namespace UiPath.Ipc; + +public interface ISerializer +{ + ValueTask DeserializeAsync(Stream json, ILogger? logger); + void Serialize(object? obj, Stream stream); + string Serialize(object? obj); + object? Deserialize(string json, Type type); +} + +internal class IpcJsonSerializer : ISerializer, 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(); + } + public char[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); + public void 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.net6.0-windows.received.txt b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt new file mode 100644 index 00000000..c99c698a --- /dev/null +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -0,0 +1,264 @@ +[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.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UiPath.Ipc.TV")] +[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 sealed class ClientConfig : UiPath.Ipc.EndpointConfig, System.IEquatable + { + public ClientConfig() { } + public string DebugName { get; set; } + public UiPath.Ipc.ISerializer? Serializer { get; set; } + public System.Func? BeforeCall { get; init; } + public System.Func? BeforeConnect { get; init; } + public UiPath.Ipc.EndpointCollection? Callbacks { get; init; } + public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; init; } + public System.IServiceProvider? ServiceProvider { get; init; } + } + public abstract class ClientTransport : System.IEquatable + { + protected ClientTransport() { } + public abstract UiPath.Ipc.IClientState CreateState(); + public abstract void Validate(); + } + public class EndpointCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + public EndpointCollection() { } + public void Add(System.Type type) { } + public void Add(UiPath.Ipc.EndpointSettings endpointSettings) { } + public void Add(System.Type contractType, object? instance) { } + public System.Collections.Generic.IEnumerator GetEnumerator() { } + } + public abstract class EndpointConfig : System.IEquatable + { + protected EndpointConfig() { } + public System.TimeSpan RequestTimeout { get; init; } + } + [System.Serializable] + public sealed class EndpointNotFoundException : System.ArgumentOutOfRangeException + { + public EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) { } + public string EndpointName { get; } + public string ServerDebugName { get; } + } + public class EndpointSettings : System.IEquatable + { + public EndpointSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } + public EndpointSettings(System.Type contractType, object? serviceInstance = null) { } + public System.Func? BeforeCall { get; set; } + public System.Type ContractType { get; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + public object? ServiceInstance { get; } + public System.IServiceProvider? ServiceProvider { get; } + public void Validate() { } + public virtual UiPath.Ipc.EndpointSettings WithServiceProvider(System.IServiceProvider? serviceProvider) { } + } + public sealed class EndpointSettings : UiPath.Ipc.EndpointSettings, System.IEquatable> + where TContract : class + { + public EndpointSettings(System.IServiceProvider serviceProvider) { } + public EndpointSettings(TContract? serviceInstance = null) { } + public override UiPath.Ipc.EndpointSettings WithServiceProvider(System.IServiceProvider? serviceProvider) { } + } + [System.Serializable] + 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 interface IClientState : System.IDisposable + { + System.IO.Stream? Network { get; } + System.Threading.Tasks.ValueTask Connect(UiPath.Ipc.IpcClient client, System.Threading.CancellationToken ct); + bool IsConnected(); + } + 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) { } + public static bool PipeExists(string pipeName, int timeout = 1) { } + } + public interface ISerializer + { + object? Deserialize(string json, System.Type type); + System.Threading.Tasks.ValueTask DeserializeAsync(System.IO.Stream json, Microsoft.Extensions.Logging.ILogger? logger); + string Serialize(object? obj); + void Serialize(object? obj, System.IO.Stream stream); + } + public sealed class IpcClient + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public IpcClient() { } + public UiPath.Ipc.ClientConfig Config { 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 : 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.EndpointCollection Endpoints { get; init; } + public System.Collections.Generic.IReadOnlyList Listeners { get; init; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; init; } + public System.IServiceProvider ServiceProvider { get; init; } + public System.Threading.Tasks.ValueTask DisposeAsync() { } + public void Start() { } + public System.Threading.Tasks.Task WaitForStart() { } + public System.Threading.Tasks.Task WaitForStop() { } + } + public abstract class ListenerConfig : UiPath.Ipc.EndpointConfig, System.IEquatable + { + protected ListenerConfig() { } + public System.Security.Cryptography.X509Certificates.X509Certificate? Certificate { get; init; } + public int ConcurrentAccepts { get; init; } + public byte MaxReceivedMessageSizeInMegabytes { get; init; } + } + 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 TCallbackInterface GetCallback() + where TCallbackInterface : class { } + public void ImpersonateClient(System.Action action) { } + } + public class Message : UiPath.Ipc.Message + { + public Message(TPayload payload) { } + public TPayload Payload { get; } + } + [System.Serializable] + 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() { } + } +} +namespace UiPath.Ipc.Extensibility +{ + public interface IListenerConfig + where TSelf : UiPath.Ipc.ListenerConfig, UiPath.Ipc.Extensibility.IListenerConfig + where TListenerState : System.IAsyncDisposable + { + System.Threading.Tasks.ValueTask AwaitConnection(TListenerState listenerState, TConnectionState connectionState, System.Threading.CancellationToken ct); + TConnectionState CreateConnectionState(UiPath.Ipc.IpcServer server, TListenerState listenerState); + TListenerState CreateListenerState(UiPath.Ipc.IpcServer server); + System.Collections.Generic.IEnumerable Validate(); + } +} +namespace UiPath.Ipc.Transport.NamedPipe +{ + public sealed class NamedPipeListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeListener() { } + [Newtonsoft.Json.JsonIgnore] + public System.Action? AccessControl { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override string ToString() { } + } + public sealed class NamedPipeTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public NamedPipeTransport() { } + public bool AllowImpersonation { get; init; } + public string PipeName { get; init; } + public string ServerName { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } + public override string ToString() { } + public override void Validate() { } + } +} +namespace UiPath.Ipc.Transport.Tcp +{ + public sealed class TcpListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpListener() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override string ToString() { } + } + public sealed class TcpTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public TcpTransport() { } + public System.Net.IPEndPoint EndPoint { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } + public override string ToString() { } + public override void Validate() { } + } +} +namespace UiPath.Ipc.Transport.WebSocket +{ + public sealed class WebSocketListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketListener() { } + public System.Func> Accept { get; init; } + public override string ToString() { } + } + public sealed class WebSocketTransport : UiPath.Ipc.ClientTransport, System.IEquatable + { + [System.Obsolete("Constructors of types with required members are not supported in this version of " + + "your compiler.", true)] + public WebSocketTransport() { } + public System.Uri Uri { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } + public override string ToString() { } + public override void Validate() { } + } +} \ 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..4efadd4f --- /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 "D:\Alt\coreipc\src\UiPath.CoreIpc\UiPath.CoreIpc.csproj" --output-directory "D:\Alt\coreipc\src\UiPath.CoreIpc\report" --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 diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.Ipc.Tests/ComputingTests.cs new file mode 100644 index 00000000..a58e6172 --- /dev/null +++ b/src/UiPath.Ipc.Tests/ComputingTests.cs @@ -0,0 +1,327 @@ +using AutoFixture; +using Newtonsoft.Json; +using Nito.AsyncEx; +using Nito.Disposables; +using NSubstitute; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Text; +using UiPath.Ipc.Transport.NamedPipe; +using UiPath.Ipc.Transport.Tcp; +using UiPath.Ipc.Transport.WebSocket; +using Xunit; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public abstract class ComputingTests : TestBase +{ + #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 readonly ConcurrentBag _clientBeforeCalls = new(); + + protected ComputingTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + ServiceProvider.InjectLazy(out _service); + CreateLazyProxy(out _proxy); + } + + protected override void ConfigureSpecificServices(IServiceCollection services) + => services + .AddSingleton() + .AddSingletonAlias() + .AddSingleton() + .AddSingletonAlias() + ; + + protected override ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + => listener with + { + ConcurrentAccepts = 10, + RequestTimeout = Timeouts.DefaultRequest, + MaxReceivedMessageSizeInMegabytes = 1, + }; + protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) + => new() + { + RequestTimeout = Timeouts.DefaultRequest, + Scheduler = GuiScheduler, + Callbacks = callbacks ?? new() + { + { typeof(IComputingCallback), _computingCallback } + }, + BeforeCall = async (callInfo, _) => _clientBeforeCalls.Add(callInfo), + }; + #endregion + + [Theory, IpcAutoData] + public async Task Calls_ShouldWork(float x, float y) + { + 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 ClientCancellations_ShouldWork() + { + 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 async Task CallbacksWithParams_ShouldWork() + => await Proxy.MultiplyInts(7, 1).ShouldBeAsync(7); + + [Fact] + public async Task ConcurrentCallbacksWithParams_ShouldWork() + => await Task.WhenAll( + Enumerable.Range(1, 50).Select(_ => CallbacksWithParams_ShouldWork())); + + [Fact] + public async Task BeforeCall_ShouldApplyToCallsButNotToToCallbacks() + { + 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 ServerBeforeCall_WhenSync_ShouldShareAsyncLocalContextWithTheTargetMethodCall() + { + await Proxy.GetCallContext().ShouldBeAsync(null); + + var id = $"{Guid.NewGuid():N}"; + var expectedCallContext = $"{nameof(IComputingService.GetCallContext)}-{id}"; + + _tailBeforeCall = (callInfo, _) => + { + ComputingService.Context = $"{callInfo.Method.Name}-{id}"; + return Task.CompletedTask; + }; + + await Proxy.GetCallContext().ShouldBeAsync(expectedCallContext); + } + + [Fact] + [OverrideConfig(typeof(SetBeforeConnect))] + public async Task BeforeConnect_ShouldWork() + { + 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(); + await Proxy.AddFloats(1, 2).ShouldBeAsync(3); + callCount.ShouldBe(2); + } + + 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() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test works only on Windows."); + + using var whereDotNet = new Process + { + StartInfo = + { + FileName = "where.exe", + Arguments = "dotnet.exe", + } + }; + 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 + { + serverProcess.Kill(); + } + catch + { + } + _outputHelper.WriteLine("Killed server process"); + }); + var proxy = new IpcClient + { + Config = new() + { + 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(); + + await proxy.AddFloats(1, 2).ShouldBeAsync(3); + } + + [SkippableFact] + public async Task ManyConnections_ShouldWork() + { + const int CParallelism = 10; + const int CTimesEach = 100; + + await Enumerable.Range(1, CParallelism) + .Select(async index => + { + var mockCallback = Substitute.For(); + mockCallback.AddInts(0, 1).Returns(1); + + var proxy = CreateClient(callbacks: new() + { + { typeof(IComputingCallback), mockCallback } + })!.GetProxy(); + + 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 ListenerConfig listener, out ClientTransport transport); + + public abstract ExternalServerParams RandomServerParams(); + public readonly record struct ExternalServerParams(ServerKind Kind, string? PipeName = null, int Port = 0) + { + public IAsyncDisposable? CreateListenerConfig(out ListenerConfig listenerConfig) + { + switch (Kind) + { + case ServerKind.NamedPipes: + { + listenerConfig = new NamedPipeListener() { PipeName = PipeName! }; + return null; + } + case ServerKind.Tcp: + { + listenerConfig = new TcpListener() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }; + return null; + } + case ServerKind.WebSockets: + { + var context = new WebSocketContext(Port); + listenerConfig = new WebSocketListener { Accept = context.Accept }; + return context; + } + default: + throw new NotSupportedException($"Kind not supported. Kind was {Kind}"); + } + } + + public ClientTransport CreateClientTransport() => Kind switch + { + ServerKind.NamedPipes => new NamedPipeTransport() { PipeName = PipeName! }, + ServerKind.Tcp => new TcpTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }, + ServerKind.WebSockets => new WebSocketTransport() { Uri = new($"ws://localhost:{Port}") }, + _ => throw new NotSupportedException($"Kind not supported. Kind was {Kind}") + }; + } + public enum ServerKind { NamedPipes, Tcp, WebSockets } + + private sealed class DisableInProcClientServer : OverrideConfig + { + public override async Task Override(Func> listener) => null; + public override IpcClient? Override(Func client) => null; + } +} diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs new file mode 100644 index 00000000..68235bae --- /dev/null +++ b/src/UiPath.Ipc.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 CreateListener() => new NamedPipeListener + { + PipeName = PipeName + }; + protected override ClientTransport CreateClientTransport() => new NamedPipeTransport + { + PipeName = PipeName, + AllowImpersonation = true, + }; + + public override IAsyncDisposable? RandomTransportPair(out ListenerConfig listener, out ClientTransport transport) + { + var pipeName = $"{Guid.NewGuid():N}"; + listener = new NamedPipeListener { PipeName = pipeName }; + transport = new NamedPipeTransport { PipeName = pipeName }; + return null; + } + + public override ExternalServerParams RandomServerParams() + => new(ServerKind.NamedPipes, PipeName: $"{Guid.NewGuid():N}"); +} diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs new file mode 100644 index 00000000..12cec28c --- /dev/null +++ b/src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs @@ -0,0 +1,35 @@ +using System.Net; +using UiPath.Ipc.Transport.Tcp; +using Xunit.Abstractions; + +namespace UiPath.Ipc.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.Ipc.Tests/ComputingTestsOverWebSockets.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs new file mode 100644 index 00000000..3b6022aa --- /dev/null +++ b/src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs @@ -0,0 +1,44 @@ +using UiPath.Ipc.Transport.WebSocket; +using Xunit.Abstractions; + +namespace UiPath.Ipc.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.Ipc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs new file mode 100644 index 00000000..a49f2aa9 --- /dev/null +++ b/src/UiPath.Ipc.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> listener) => listener()!; + public virtual IpcClient? Override(Func client) => client(); +} \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/GlobalUsings.cs b/src/UiPath.Ipc.Tests/GlobalUsings.cs new file mode 100644 index 00000000..364e8e60 --- /dev/null +++ b/src/UiPath.Ipc.Tests/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using Accept = System.Func>; +global using BeforeConnectHandler = System.Func; +global using BeforeCallHandler = System.Func; diff --git a/src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs b/src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs new file mode 100644 index 00000000..fd750042 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs b/src/UiPath.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs new file mode 100644 index 00000000..257e9a8b --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs b/src/UiPath.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs new file mode 100644 index 00000000..2ce875a1 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs new file mode 100644 index 00000000..0296354b --- /dev/null +++ b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs @@ -0,0 +1,57 @@ +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) + => new() + { + Config = ipcClient.Config with { RequestTimeout = requestTimeout }, + Transport = ipcClient.Transport, + }; + + public static IpcClient WithCallbacks(this IpcClient ipcClient, EndpointCollection callbacks) + => new() + { + Config = ipcClient.Config with { Callbacks = callbacks }, + Transport = ipcClient.Transport, + }; + + public static IpcClient WithBeforeConnect(this IpcClient ipcClient, BeforeConnectHandler beforeConnect) + => new() + { + Config = ipcClient.Config with { BeforeConnect = beforeConnect }, + Transport = ipcClient.Transport, + }; +} \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/Helpers/Names.cs b/src/UiPath.Ipc.Tests/Helpers/Names.cs new file mode 100644 index 00000000..2668055b --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/NetworkHelper.cs b/src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs new file mode 100644 index 00000000..327ffbd1 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/ShouldlyHelpers.cs b/src/UiPath.Ipc.Tests/Helpers/ShouldlyHelpers.cs new file mode 100644 index 00000000..0895c94d --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/StreamBase.cs b/src/UiPath.Ipc.Tests/Helpers/StreamBase.cs new file mode 100644 index 00000000..cbf2c599 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/StreamExtensions.cs b/src/UiPath.Ipc.Tests/Helpers/StreamExtensions.cs new file mode 100644 index 00000000..82174e14 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/TestRunId.cs b/src/UiPath.Ipc.Tests/Helpers/TestRunId.cs new file mode 100644 index 00000000..f76d43c8 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/Timeouts.cs b/src/UiPath.Ipc.Tests/Helpers/Timeouts.cs new file mode 100644 index 00000000..d09e1e2e --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/TracedStream.cs b/src/UiPath.Ipc.Tests/Helpers/TracedStream.cs new file mode 100644 index 00000000..e9648056 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Helpers/WebSocketContext.cs b/src/UiPath.Ipc.Tests/Helpers/WebSocketContext.cs new file mode 100644 index 00000000..5fb55217 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs new file mode 100644 index 00000000..87f90be6 --- /dev/null +++ b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs @@ -0,0 +1,79 @@ +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 + { + Listeners = [ + new NamedPipeListener + { + PipeName = pipeName, + } + ], + Endpoints = new() + { + typeof(IComputingService) + }, + ServiceProvider = new ServiceCollection() + .AddLogging() + .AddSingleton() + .BuildServiceProvider() + }; + + private static IpcClient CreateClient(string pipeName) + => new() + { + Transport = new NamedPipeTransport { PipeName = pipeName }, + Config = new() + }; + + 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.Ipc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.Ipc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..0032e3dd --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Polyfills/IsExternalInit.cs b/src/UiPath.Ipc.Tests/Polyfills/IsExternalInit.cs new file mode 100644 index 00000000..dc521fbf --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Polyfills/System_Index.cs b/src/UiPath.Ipc.Tests/Polyfills/System_Index.cs new file mode 100644 index 00000000..177b155c --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Program.cs b/src/UiPath.Ipc.Tests/Program.cs new file mode 100644 index 00000000..6e917ad3 --- /dev/null +++ b/src/UiPath.Ipc.Tests/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Text; +using UiPath.Ipc; +using UiPath.Ipc.Tests; + +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 listener); + +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) }, + }, + Listeners = [listener], +}; +ipcServer.Start(); +await ipcServer.WaitForStop(); + +return 0; \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/RobotTests.cs b/src/UiPath.Ipc.Tests/RobotTests.cs new file mode 100644 index 00000000..f632f26c --- /dev/null +++ b/src/UiPath.Ipc.Tests/RobotTests.cs @@ -0,0 +1,72 @@ +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 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 ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + => listener with + { + ConcurrentAccepts = 10, + RequestTimeout = Timeouts.DefaultRequest, + MaxReceivedMessageSizeInMegabytes = 1, + }; + protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) + => new() + { + RequestTimeout = Timeouts.DefaultRequest, + Scheduler = GuiScheduler, + Callbacks = callbacks ?? new() + { + { typeof(IStudioEvents), _studioEvents } + }, + BeforeCall = async (callInfo, _) => _clientBeforeCalls.Add(callInfo), + }; + #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.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs new file mode 100644 index 00000000..78d607a0 --- /dev/null +++ b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs @@ -0,0 +1,293 @@ +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 CreateListener() => new NamedPipeListener + { + PipeName = PipeName + }; + protected override ClientTransport CreateClientTransport() => new NamedPipeTransport + { + 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( + listener: new NamedPipeListener() + { + PipeName = pipeName, + AccessControl = security => security.AllowCurrentUser(), + MaxReceivedMessageSizeInMegabytes = 10, + RequestTimeout = TimeSpan.FromSeconds(40), + }, + callbacks: new EndpointCollection() + { + { typeof(TCallback), Instance } + }, + beforeConnect: Communication.OnConnectingToUserService); + } +} + +internal static partial class RobotIpcHelpers +{ + private static readonly ConcurrentDictionary PipeClients = new(); + + public static TContract CreateProxy( + NamedPipeListener listener, + EndpointCollection? callbacks = null, + IServiceProvider? provider = null, + Action? beforeConnect = null, + BeforeCallHandler? beforeCall = null, + bool allowImpersonation = false, + TaskScheduler? scheduler = null) where TContract : class + => CreateProxy(listener.PipeName, listener.RequestTimeout, callbacks, provider, beforeConnect, beforeCall, allowImpersonation, scheduler); + + public static TContract CreateProxy( + string pipeName, + TimeSpan? requestTimeout = null, + EndpointCollection? callbacks = null, + IServiceProvider? provider = null, + Action? beforeConnect = null, + BeforeCallHandler? beforeCall = null, + bool allowImpersonation = false, + TaskScheduler? scheduler = null) where TContract : class + { + // 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) + { + throw requestedParams - originalParams; + } + + return client.GetProxy(); + } + + private static ClientAndParams CreateClient(CreateProxyRequest request) + => new( + Client: new() + { + Config = 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; + }, + BeforeCall = request.Params.BeforeCall, + Scheduler = request.Params.Scheduler, + }, + Transport = new NamedPipeTransport + { + 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, EndpointCollection? 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(EndpointCollection? 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(EndpointSettings 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.Ipc.Tests/Services/ArithmeticCallback.cs b/src/UiPath.Ipc.Tests/Services/ArithmeticCallback.cs new file mode 100644 index 00000000..4493a934 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Services/ComputingCallback.cs b/src/UiPath.Ipc.Tests/Services/ComputingCallback.cs new file mode 100644 index 00000000..630a8c71 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Services/ComputingService.cs b/src/UiPath.Ipc.Tests/Services/ComputingService.cs new file mode 100644 index 00000000..40f1863a --- /dev/null +++ b/src/UiPath.Ipc.Tests/Services/ComputingService.cs @@ -0,0 +1,66 @@ +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) + { + logger.LogInformation($"{nameof(AddComplexNumbers)} called."); + return a + b; + } + + 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.GetCallback().GetThreadName(); + } + + public async Task AddComplexNumberList(IReadOnlyList numbers) + { + var result = ComplexNumber.Zero; + foreach (var number in numbers) + { + result += number; + } + return result; + } + + public async Task MultiplyInts(int x, int y, Message message = null!) + { + var callback = message.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; + } +} diff --git a/src/UiPath.Ipc.Tests/Services/IComputingService.cs b/src/UiPath.Ipc.Tests/Services/IComputingService.cs new file mode 100644 index 00000000..145c00f6 --- /dev/null +++ b/src/UiPath.Ipc.Tests/Services/IComputingService.cs @@ -0,0 +1,48 @@ + +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); + Task Wait(TimeSpan duration, CancellationToken ct = default); + Task GetCallbackThreadName(TimeSpan waitOnServer, Message message = null!, CancellationToken cancellationToken = default); + Task AddComplexNumberList(IReadOnlyList numbers); + Task MultiplyInts(int x, int y, Message message = null!); + Task GetCallContext(); +} + +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.Ipc.Tests/Services/ISystemService.cs b/src/UiPath.Ipc.Tests/Services/ISystemService.cs new file mode 100644 index 00000000..06714a5f --- /dev/null +++ b/src/UiPath.Ipc.Tests/Services/ISystemService.cs @@ -0,0 +1,45 @@ +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); + + 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!); +} + +public interface IUnregisteredCallback +{ + Task SomeMethod(); +} diff --git a/src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs b/src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs new file mode 100644 index 00000000..2469b06a --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Services/Robot/Impl.cs b/src/UiPath.Ipc.Tests/Services/Robot/Impl.cs new file mode 100644 index 00000000..f02fdea9 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/Services/Robot/Pals.cs b/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs new file mode 100644 index 00000000..5f2daea2 --- /dev/null +++ b/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs @@ -0,0 +1,97 @@ +using Nito.AsyncEx; +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 + { + callback = _callbacks.FirstOrDefault(c => c.Client == message.Client); + if (callback != null) + { + 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.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.Ipc.Tests/Services/SystemService.cs b/src/UiPath.Ipc.Tests/Services/SystemService.cs new file mode 100644 index 00000000..9f1d9394 --- /dev/null +++ b/src/UiPath.Ipc.Tests/Services/SystemService.cs @@ -0,0 +1,82 @@ +using System.Buffers; +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.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.GetCallback().AddInts(x, y); + var result = await message.GetCallback().Increment(sum); + return result; + } +} diff --git a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs new file mode 100644 index 00000000..416f2fb5 --- /dev/null +++ b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs @@ -0,0 +1,147 @@ +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 + { + Listeners = [ + new NamedPipeListener + { + PipeName = pipeName, + } + ], + Endpoints = new() + { + typeof(IComputingService) + }, + ServiceProvider = new ServiceCollection() + .AddLogging() + .AddSingleton() + .BuildServiceProvider() + }; + + private static IpcClient CreateClient(string pipeName) + => new() + { + Transport = new NamedPipeTransport { PipeName = pipeName }, + Config = new() + }; + + + + 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.Ipc.Tests/SystemTests.cs b/src/UiPath.Ipc.Tests/SystemTests.cs new file mode 100644 index 00000000..a00f97a6 --- /dev/null +++ b/src/UiPath.Ipc.Tests/SystemTests.cs @@ -0,0 +1,433 @@ +using AutoFixture; +using AutoFixture.Xunit2; +using Microsoft.Extensions.Hosting; +using System.Text; +using System.Threading.Channels; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public abstract class SystemTests : TestBase +{ + #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) + { + ServiceProvider.InjectLazy(out _service); + CreateLazyProxy(out _proxy); + } + + protected override void ConfigureSpecificServices(IServiceCollection services) + => services + .AddSingleton() + .AddSingletonAlias(); + + protected override ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + => listener with + { + ConcurrentAccepts = 10, + RequestTimeout = Timeouts.DefaultRequest, + MaxReceivedMessageSizeInMegabytes = 1, + }; + protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) + => new() + { + RequestTimeout = Timeouts.DefaultRequest, + ServiceProvider = ServiceProvider, + Callbacks = callbacks + }; + #endregion + + [Theory, IpcAutoData] + public async Task PassingArgsAndReturning_ShouldWork(Guid guid) + { + var clone = await Proxy.EchoGuidAfter(guid, TimeSpan.Zero); + clone.ShouldBe(guid); + } + + [Theory, IpcAutoData] + public async Task ConcurrentOperations_ShouldWork(Guid guid1, Guid guid2) + { + 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 NotPassingAnOptionalMessage_ShouldWork() + => await Proxy + .MessageReceivedAsNotNull(message: null) + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip) + .ShouldNotThrowAsyncAnd() + .ShouldBeAsync(true); + + [Fact] + [OverrideConfig(typeof(ServerExecutingTooLongACall_ShouldThrowTimeout_Config))] + 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] + [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 ServerExecutingTooLongACall_ShouldThrowTimeout_Config : OverrideConfig + { + public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeouts.Short }; + public override IpcClient? Override(Func client) + => client().WithRequestTimeout(Timeout.InfiniteTimeSpan); + } + + private sealed class ClientWaitingForTooLongACall_ShouldThrowTimeout_Config : OverrideConfig + { + public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeout.InfiniteTimeSpan }; + public override IpcClient? Override(Func client) + => client().WithRequestTimeout(Timeouts.IpcRoundtrip); + } + + private ListenerConfig ShortClientTimeout(ListenerConfig listener) => listener with { RequestTimeout = TimeSpan.FromMilliseconds(100) }; + private ListenerConfig InfiniteServerTimeout(ListenerConfig listener) => listener with { RequestTimeout = Timeout.InfiniteTimeSpan }; + + [Fact] + public async Task FireAndForget_ShouldWork() + { + 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 ExceedingMsgSize_ShouldBreakNetwork_ButShouldBeRecoverable() + { + 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 ServerCallingInexistentCallback_ShouldThrow() + { + var (exceptionType, exceptionMessage, marshalledExceptionType) = (await Proxy.CallUnregisteredCallback()).ShouldNotBeNull(); + exceptionType.ShouldBe(nameof(RemoteException)); + marshalledExceptionType.ShouldBe(typeof(EndpointNotFoundException).FullName); + } + + [Fact] + 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 + { + public override IpcClient? Override(Func client) + => client().WithCallbacks(new() + { + { typeof(IComputingCallback), new ComputingCallback() }, + { typeof(IArithmeticCallback), new ArithmeticCallback() }, + }); + } + + [Fact] + public async Task FireAndForgetOperations_ShouldNotDeliverBusinessExceptionsEvenWhenThrownSynchronously() + => await Proxy.FireAndForgetThrowSync() + .ShouldNotThrowAsync() + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip); + + [Fact] + public async Task ServerScheduler_ShouldBeUsed() + => await Proxy.GetThreadName() + .ShouldBeAsync(Names.GuiThreadName); + + [Theory, IpcAutoData] + public async Task UploadingStreams_ShouldWork(string str) + { + using var memory = new MemoryStream(Encoding.UTF8.GetBytes(str)); + await Proxy.UploadEcho(memory).ShouldBeAsync(str); + } + + //[Theory, IpcAutoData] + public async Task CancelingStreamUploads_ShouldThrow(string str, Guid guid) + { + var sourceMemory = new Memory(Encoding.UTF8.GetBytes(str)); + + using var cts = new CancellationTokenSource(); + using var stream = new UploadStream(); + + var taskReadCall = stream.AwaitReadCall(); + + var taskUploading = Proxy.UploadEcho(stream, cts.Token); + + 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) + { + 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)); + } + +#if !CI + [Theory, IpcAutoData] +#endif + public async Task UnfinishedUploads_ShouldThrowOnTheClient_AndRecover_Repeat(Guid guid) + { + const int IterationCount = 500; + foreach (var i in Enumerable.Range(1, IterationCount)) + { + _outputHelper.WriteLine($"Starting iteration {i}/{IterationCount}..."); + await UnfinishedUploads_ShouldThrowOnTheClient_AndRecover(guid); + _outputHelper.WriteLine($"Finished iteration {i}/{IterationCount}."); + } + } + + [Theory, IpcAutoData] + public async Task DownloadingStreams_ShouldWork(string str) + { + using var stream = await Proxy.Download(str); + using var reader = new StreamReader(stream); + var clone = await reader.ReadToEndAsync(); + clone.ShouldBe(str); + } + + public static IEnumerable DownloadingStreams_ShouldWork_Repeat_Cases() + { + var fixture = IpcAutoDataAttribute.CreateFixture(); + const int CTimes = 100; + + foreach (var time in Enumerable.Range(1, CTimes)) + { + yield return [time, fixture.Create()]; + } + } + + [Theory, IpcAutoData] + public async Task StreamDownloadsClosedUnfinished_ShouldNotAffectTheConnection(string str, Guid guid) + { + using (var stream = await Proxy.Download(str)) + { + } + + await Proxy.EchoGuidAfter(guid, TimeSpan.Zero) + .ShouldBeAsync(guid) + .ShouldCompleteInAsync(Timeouts.IpcRoundtrip); + } + + [Theory, IpcAutoData] + public async Task StreamDownloadsLeftOpen_WillHijackTheConnection(string str, Guid guid) + { + using (var stream = await Proxy.Download(str)) + { + await new StreamReader(stream).ReadToEndAsync() + .ShouldBeAsync(str); + + await Proxy.EchoGuidAfter(guid, waitOnServer: TimeSpan.Zero, message: new() { RequestTimeout = Timeout.InfiniteTimeSpan }) + .ShouldStallForAtLeastAsync(Timeouts.IpcRoundtrip); + } + } + +#if !CI + [Theory, IpcAutoData] +#endif + public async Task StreamDownloadsLeftOpen_WillHijackTheConnection_Repeat(string str, Guid guid) + { + const int IterationCount = 20; + foreach (var i in Enumerable.Range(0, IterationCount)) + { + await StreamDownloadsLeftOpen_WillHijackTheConnection(str, guid); + } + } + + [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()) + { + await host.StartAsync(); + var hostedIpcServer = host.Services.GetRequiredService(); + hostedIpcServer.Set(IpcServer!); + await host.StopAsync(); + } + + await IpcServer!.DisposeAsync(); + await IpcServer!.DisposeAsync(); + await infiniteTask.ShouldThrowAsync().ShouldCompleteInAsync(Timeouts.IpcRoundtrip); + } + + private sealed class UploadStream : StreamBase + { + 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) + { + 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); + } + } + + private interface IHostedIpcServer + { + void Set(IpcServer ipcServer); + } + + private sealed class HostedIpcServer : IHostedService, IHostedIpcServer, IAsyncDisposable + { + 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 + { + await _ipcServer!.DisposeAsync(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs new file mode 100644 index 00000000..ebcdac6c --- /dev/null +++ b/src/UiPath.Ipc.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 CreateListener() => new NamedPipeListener + { + PipeName = PipeName + }; + protected sealed override ClientTransport CreateClientTransport() => new NamedPipeTransport() + { + PipeName = PipeName, + AllowImpersonation = true, + }; +} diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs new file mode 100644 index 00000000..d1fefa1f --- /dev/null +++ b/src/UiPath.Ipc.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 CreateListener() + => new TcpListener + { + EndPoint = _endPoint, + }; + + protected override ClientTransport CreateClientTransport() + => new TcpTransport() { EndPoint = _endPoint }; +} diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs new file mode 100644 index 00000000..09598cf6 --- /dev/null +++ b/src/UiPath.Ipc.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 CreateListener() + { + var listener = new WebSocketListener + { + Accept = _webSocketContext.Accept, + ConcurrentAccepts = 1, + }; + await Task.Delay(500); // Wait for the listener to start. + return listener; + } + + protected override ClientTransport CreateClientTransport() + => new WebSocketTransport() { Uri = _webSocketContext.ClientUri }; +} diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.Ipc.Tests/TestBase.cs new file mode 100644 index 00000000..783bf6fa --- /dev/null +++ b/src/UiPath.Ipc.Tests/TestBase.cs @@ -0,0 +1,160 @@ +using Nito.AsyncEx; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Xunit.Abstractions; + +namespace UiPath.Ipc.Tests; + +public abstract class TestBase : IAsyncLifetime +{ + 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 + ; + _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(CreateServer); + _ipcClient = new(() => CreateClient()); + + 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); + + private Task CreateListenerAndConfigure() + { + var factory = async () => + { + _outputHelper.WriteLine("Creating listener..."); + var listener = await CreateListener(); + listener = ConfigTransportAgnostic(listener); + return listener; + }; + + if (_overrideConfig is null) + { + return factory(); + } + + return _overrideConfig.Override(factory); + } + + protected async Task CreateServer() + { + if (await CreateListenerAndConfigure() is not { } listener) return null; + + return new() + { + Endpoints = new() { + new EndpointSettings(ContractType) + { + BeforeCall = (callInfo, ct) => + { + _serverBeforeCalls.Add(callInfo); + return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask; + } + } + }, + Listeners = [listener], + ServiceProvider = _serviceProvider, + Scheduler = GuiScheduler + }; + } + + protected IpcClient? CreateClient(EndpointCollection? callbacks = null) + { + var factory = () => + { + var config = CreateClientConfig(callbacks); + var transport = CreateClientTransport(); + var client = new IpcClient + { + Config = config, + Transport = transport + }; + return client; + }; + + if (_overrideConfig is null) + { + return factory(); + } + + return _overrideConfig.Override(factory); + } + protected TContract? GetProxy() where TContract : class + => _ipcClient.Value?.GetProxy(); + + protected void CreateLazyProxy(out Lazy lazy) where TContract : class => lazy = new(GetProxy); + + protected abstract Task CreateListener(); + + protected abstract ClientConfig CreateClientConfig(EndpointCollection? callbacks = null); + protected abstract ClientTransport CreateClientTransport(); + + protected abstract ListenerConfig ConfigTransportAgnostic(ListenerConfig listener); + + protected virtual async Task DisposeAsync() + { + IpcProxy?.Dispose(); + await (IpcProxy?.CloseConnection() ?? default); + await (IpcServer?.DisposeAsync() ?? default); + _guiThread.Dispose(); + await _serviceProvider.DisposeAsync(); + } + + async Task IAsyncLifetime.InitializeAsync() + { + IpcServer = await _ipcServer.Value; + IpcServer?.Start(); + await (IpcServer?.WaitForStart() ?? Task.CompletedTask); + } + + Task IAsyncLifetime.DisposeAsync() => DisposeAsync(); +} diff --git a/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj b/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj new file mode 100644 index 00000000..3f326d6d --- /dev/null +++ b/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj @@ -0,0 +1,56 @@ + + + + WinExe + net6.0;net461 + UiPath.Ipc.Tests + $(NoWarn);1998 + $(DefineConstants);$(DefineConstantsEx) + latest + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs b/src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs new file mode 100644 index 00000000..b954340b --- /dev/null +++ b/src/UiPath.Ipc.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 From 6e8f2586205d9df79c33c5d7da6056d435961a07 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 19 Nov 2024 11:29:44 +0100 Subject: [PATCH 02/26] taking shape --- src/Playground/Program.cs | 2 +- src/UiPath.CoreIpc.Http/BidiHttpListener.cs | 2 +- src/UiPath.CoreIpc/Config/ClientConfig.cs | 2 +- src/UiPath.CoreIpc/Config/IListenerConfig.cs | 4 +- src/UiPath.CoreIpc/Config/IpcClient.cs | 4 +- src/UiPath.CoreIpc/Config/IpcServer.cs | 58 ++------ .../Config/{EndpointConfig.cs => Peer.cs} | 6 +- .../{ListenerConfig.cs => ServerTransport.cs} | 4 +- src/UiPath.CoreIpc/Helpers/Result.cs | 21 +++ src/UiPath.CoreIpc/Server/Listener.cs | 127 ++++++++---------- src/UiPath.CoreIpc/Server/ServerConnection.cs | 2 +- .../Transport/NamedPipe/NamedPipeListener.cs | 2 +- .../Transport/Tcp/TcpListener.cs | 2 +- .../Transport/WebSocket/WebSocketListener.cs | 2 +- src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs | 2 +- src/UiPath.Ipc.Tests/ComputingTests.cs | 8 +- .../ComputingTestsOverNamedPipes.cs | 4 +- .../Config/OverrideConfigAttribute.cs | 2 +- src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs | 2 +- src/UiPath.Ipc.Tests/Program.cs | 2 +- src/UiPath.Ipc.Tests/RobotTests.cs | 2 +- .../RobotTestsOverNamedPipes.cs | 2 +- src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs | 2 +- src/UiPath.Ipc.Tests/SystemTests.cs | 10 +- .../SystemTestsOverNamedPipes.cs | 2 +- src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs | 2 +- .../SystemTestsOverWebSockets.cs | 2 +- src/UiPath.Ipc.Tests/TestBase.cs | 8 +- 28 files changed, 135 insertions(+), 153 deletions(-) rename src/UiPath.CoreIpc/Config/{EndpointConfig.cs => Peer.cs} (60%) rename src/UiPath.CoreIpc/Config/{ListenerConfig.cs => ServerTransport.cs} (90%) create mode 100644 src/UiPath.CoreIpc/Helpers/Result.cs diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index 86bb8470..df10be22 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -50,7 +50,7 @@ private static async Task Main(string[] args) }, typeof(Contracts.IClientOperations2) }, - Listeners = [ + Transport = [ new NamedPipeListener() { PipeName = Contracts.PipeName, diff --git a/src/UiPath.CoreIpc.Http/BidiHttpListener.cs b/src/UiPath.CoreIpc.Http/BidiHttpListener.cs index e5c299df..06076a21 100644 --- a/src/UiPath.CoreIpc.Http/BidiHttpListener.cs +++ b/src/UiPath.CoreIpc.Http/BidiHttpListener.cs @@ -12,7 +12,7 @@ namespace UiPath.Ipc.Http; using static Constants; using IBidiHttpListenerConfig = IListenerConfig; -public sealed partial record BidiHttpListener : ListenerConfig, IBidiHttpListenerConfig +public sealed partial record BidiHttpListener : ServerTransport, IBidiHttpListenerConfig { public required Uri Uri { get; init; } diff --git a/src/UiPath.CoreIpc/Config/ClientConfig.cs b/src/UiPath.CoreIpc/Config/ClientConfig.cs index 6b0191ac..7d77abfa 100644 --- a/src/UiPath.CoreIpc/Config/ClientConfig.cs +++ b/src/UiPath.CoreIpc/Config/ClientConfig.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -public sealed record ClientConfig : EndpointConfig, IServiceClientConfig +public sealed record ClientConfig : Peer, IServiceClientConfig { public EndpointCollection? Callbacks { get; init; } diff --git a/src/UiPath.CoreIpc/Config/IListenerConfig.cs b/src/UiPath.CoreIpc/Config/IListenerConfig.cs index a9513957..dd8976cc 100644 --- a/src/UiPath.CoreIpc/Config/IListenerConfig.cs +++ b/src/UiPath.CoreIpc/Config/IListenerConfig.cs @@ -1,7 +1,7 @@ namespace UiPath.Ipc.Extensibility; -public interface IListenerConfig - where TSelf : ListenerConfig, IListenerConfig +internal interface IListenerConfig + where TSelf : ServerTransport, IListenerConfig where TListenerState : IAsyncDisposable { TListenerState CreateListenerState(IpcServer server); diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index d09c7e08..24104b16 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -1,9 +1,7 @@ namespace UiPath.Ipc; -public sealed class IpcClient +public sealed class IpcClient : Peer { - private ClientTransport _transport = null!; - public required ClientConfig Config { get; init; } public required ClientTransport Transport { get; init; } diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index d4347d96..a9485a3b 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -1,16 +1,17 @@ namespace UiPath.Ipc; -public sealed class IpcServer : IAsyncDisposable +public sealed class IpcServer : Peer, IAsyncDisposable { - public required IServiceProvider ServiceProvider { get; init; } public required EndpointCollection Endpoints { get; init; } - public required IReadOnlyList Listeners { get; init; } - public TaskScheduler? Scheduler { get; init; } + public required ServerTransport Transport { get; init; } - private readonly Lazy> _started; + private readonly Lazy _listener; private readonly TaskCompletionSource _tcsStopped = new(); - public IpcServer() => _started = new(StartCore); + public IpcServer() + { + _listener = new(() => Listener.Create(server: this)); + } public void Start() { @@ -24,25 +25,21 @@ public void Start() public Task WaitForStart() => _started.Value; public Task WaitForStop() => _tcsStopped.Task; - private async Task StartCore() + private Listener? StartCore() { if (!IsValid(out _)) { return null; } - var disposables = new StopAdapter(this); try { - foreach (var listenerConfig in Listeners) - { - disposables.Add(Listener.Create(this, listenerConfig)); - } - return disposables; + return Listener.Create(this, Transport); } catch (Exception ex) { Trace.TraceError($"Failed to start server. Ex: {ex}"); + _tcsStopped.TrySetException(ex); disposables.SetException(ex); await disposables.DisposeAsync(); throw; @@ -51,11 +48,11 @@ public void Start() private bool IsValid(out IReadOnlyList errors) { - errors = Listeners.SelectMany(PrefixErrors).ToArray(); + errors = PrefixErrors(Transport).ToArray(); return errors is { Count: 0 }; - static IEnumerable PrefixErrors(ListenerConfig config) - => config.Validate().Select(error => $"{config.GetType().Name}: {error}"); + static IEnumerable PrefixErrors(ServerTransport transport) + => transport.Validate().Select(error => $"{transport.GetType().Name}: {error}"); } public async ValueTask DisposeAsync() @@ -64,33 +61,4 @@ public async ValueTask DisposeAsync() await ((await _started.Value)?.DisposeAsync() ?? default); } - - private sealed class StopAdapter : IAsyncDisposable - { - private readonly List _items = new(); - private readonly IpcServer _server; - private Exception? _exception; - - public StopAdapter(IpcServer server) => _server = server; - - public void Add(IAsyncDisposable item) => _items.Add(item); - - public void SetException(Exception ex) => _exception = ex; - - public async ValueTask DisposeAsync() - { - foreach (var item in _items) - { - await item.DisposeAsync(); - } - - if (_exception is not null) - { - _server._tcsStopped.TrySetException(_exception); - return; - } - - _server._tcsStopped.TrySetResult(null); - } - } } diff --git a/src/UiPath.CoreIpc/Config/EndpointConfig.cs b/src/UiPath.CoreIpc/Config/Peer.cs similarity index 60% rename from src/UiPath.CoreIpc/Config/EndpointConfig.cs rename to src/UiPath.CoreIpc/Config/Peer.cs index 4a06504a..432e3a86 100644 --- a/src/UiPath.CoreIpc/Config/EndpointConfig.cs +++ b/src/UiPath.CoreIpc/Config/Peer.cs @@ -1,8 +1,12 @@ namespace UiPath.Ipc; -public abstract record EndpointConfig +public abstract class Peer { public TimeSpan RequestTimeout { get; init; } = Timeout.InfiniteTimeSpan; + public IServiceProvider? ServiceProvider { get; init; } + public TaskScheduler? Scheduler { get; init; } + + internal ISerializer? Serializer => IpcJsonSerializer.Instance; internal virtual RouterConfig CreateRouterConfig(IpcServer server) => throw new NotSupportedException(); diff --git a/src/UiPath.CoreIpc/Config/ListenerConfig.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs similarity index 90% rename from src/UiPath.CoreIpc/Config/ListenerConfig.cs rename to src/UiPath.CoreIpc/Config/ServerTransport.cs index 2c41dcc6..4c15ec53 100644 --- a/src/UiPath.CoreIpc/Config/ListenerConfig.cs +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -public abstract record ListenerConfig : EndpointConfig, IServiceClientConfig +public abstract class ServerTransport : Peer, IServiceClientConfig { public int ConcurrentAccepts { get; init; } = 5; public byte MaxReceivedMessageSizeInMegabytes { get; init; } = 2; @@ -20,7 +20,7 @@ internal override RouterConfig CreateRouterConfig(IpcServer server) }); #region IServiceClientConfig - /// Do not implement explicitly, as it must be implicitly implemented by . + /// Do not implement explicitly, as it must be implicitly implemented by . BeforeConnectHandler? IServiceClientConfig.BeforeConnect => null; BeforeCallHandler? IServiceClientConfig.BeforeCall => null; 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/Server/Listener.cs b/src/UiPath.CoreIpc/Server/Listener.cs index 9f999d69..df472011 100644 --- a/src/UiPath.CoreIpc/Server/Listener.cs +++ b/src/UiPath.CoreIpc/Server/Listener.cs @@ -2,87 +2,29 @@ namespace UiPath.Ipc; -using ListenerFactory = Func; +using ListenerFactory = Func; internal abstract class Listener : IAsyncDisposable { - public static Listener Create(IpcServer server, ListenerConfig config) - => GetFactory(config.GetType())( - server ?? throw new ArgumentNullException(nameof(server)), - config ?? throw new ArgumentNullException(nameof(config))); + private static readonly GenericListenerFactoryCache Cache = new(); - private readonly struct Result + public static Listener Create(IpcServer server) { - 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; - } - } - private static readonly ConcurrentDictionary> Factories = new(); - private static ListenerFactory GetFactory(Type configType) => Factories.GetOrAdd(configType, CreateFactory).Value; - private static Result CreateFactory(Type configType) - { - try - { - return new(Pal(configType)); - } - catch (Exception ex) - { - return new(ex); - } - - static ListenerFactory Pal(Type configType) - { - if (configType.GetInterfaces().SingleOrDefault(IsIListenerConfig) is not { } iface - || iface.GetGenericArguments() is not [_, var listenerStateType, var connectionStateType]) - { - throw new ArgumentOutOfRangeException(nameof(iface), $"The ListenerConfig type must implement IListenerConfig<,>. ListenerConfig type was: {configType.FullName}"); - } - - var listenerType = typeof(Listener<,,>).MakeGenericType(configType, listenerStateType, connectionStateType); - var listenerCtor = listenerType.GetConstructor( - bindingAttr: BindingFlags.Public | BindingFlags.Instance, - binder: Type.DefaultBinder, - types: [typeof(IpcServer), configType], - modifiers: null)!; - - var paramofServer = Expression.Parameter(typeof(IpcServer)); - var paramofConfig = Expression.Parameter(typeof(ListenerConfig)); - var lambda = Expression.Lambda( - delegateType: typeof(ListenerFactory), - body: Expression.New(listenerCtor, paramofServer, Expression.Convert(paramofConfig, configType)), - paramofServer, - paramofConfig); - - var @delegate = (lambda.Compile() as ListenerFactory)!; - return @delegate; - } - - static bool IsIListenerConfig(Type candidateIface) - => candidateIface.IsGenericType && candidateIface.GetGenericTypeDefinition() == typeof(IListenerConfig<,,>); + var transportType = server.Transport.GetType(); + var listenerFactory = Cache.Get(transportType); + var listener = listenerFactory(server); + return listener; } private readonly Lazy _disposeTask = null!; - public readonly ListenerConfig Config; + public readonly ServerTransport Config; public readonly IpcServer Server; private readonly Lazy _loggerCategory; private readonly Lazy _lazyLogger; public ILogger Logger => _lazyLogger.Value; - protected Listener(IpcServer server, ListenerConfig config) + protected Listener(IpcServer server, ServerTransport config) { _loggerCategory = new(ComputeLoggerCategory); Config = config; @@ -104,7 +46,7 @@ private string ComputeLoggerCategory() } internal sealed class Listener : Listener, IAsyncDisposable - where TConfig : ListenerConfig, IListenerConfig + where TConfig : ServerTransport, IListenerConfig where TListenerState : IAsyncDisposable where TConnectionState : IDisposable { @@ -224,3 +166,52 @@ async Task TryToListen() protected override Type ConfigType => typeof(TConfig); } + +internal sealed class GenericListenerFactoryCache +{ + private readonly ConcurrentDictionary> _cache = new(); + + public ListenerFactory Get(Type configType) => _cache.GetOrAdd(configType, Create).Value; + + private Result Create(Type configType) + { + try + { + return new(Emit(configType)); + } + catch (Exception ex) + { + return new(ex); + } + + static ListenerFactory Emit(Type configType) + { + if (configType.GetInterfaces().SingleOrDefault(IsIListenerConfig) is not { } iface + || iface.GetGenericArguments() is not [_, var listenerStateType, var connectionStateType]) + { + throw new ArgumentOutOfRangeException(nameof(iface), $"The ListenerConfig type must implement IListenerConfig<,>. ListenerConfig type was: {configType.FullName}"); + } + + var listenerType = typeof(Listener<,,>).MakeGenericType(configType, listenerStateType, connectionStateType); + var listenerCtor = listenerType.GetConstructor( + bindingAttr: BindingFlags.Public | BindingFlags.Instance, + binder: Type.DefaultBinder, + types: [typeof(IpcServer), configType], + modifiers: null)!; + + var paramofServer = Expression.Parameter(typeof(IpcServer)); + var paramofConfig = Expression.Parameter(typeof(ServerTransport)); + var lambda = Expression.Lambda( + delegateType: typeof(ListenerFactory), + body: Expression.New(listenerCtor, paramofServer, Expression.Convert(paramofConfig, configType)), + paramofServer, + paramofConfig); + + var @delegate = (lambda.Compile() as ListenerFactory)!; + return @delegate; + } + + static bool IsIListenerConfig(Type candidateIface) + => candidateIface.IsGenericType && candidateIface.GetGenericTypeDefinition() == typeof(IListenerConfig<,,>); + } +} diff --git a/src/UiPath.CoreIpc/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index 84aafe9e..be2c90c8 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -9,7 +9,7 @@ public interface IClient } internal sealed class ServerConnection : ServerConnection - where TConfig : ListenerConfig, IListenerConfig + where TConfig : ServerTransport, IListenerConfig where TListenerState : IAsyncDisposable where TConnectionState : IDisposable { diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs index 4e687558..219fc7a6 100644 --- a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs @@ -6,7 +6,7 @@ namespace UiPath.Ipc.Transport.NamedPipe; using INamedPipeListenerConfig = IListenerConfig; -public sealed record NamedPipeListener : ListenerConfig, INamedPipeListenerConfig +public sealed record NamedPipeListener : ServerTransport, INamedPipeListenerConfig { public required string PipeName { get; init; } public string ServerName { get; init; } = "."; diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs index d9de0eaf..fc146f5a 100644 --- a/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs @@ -5,7 +5,7 @@ namespace UiPath.Ipc.Transport.Tcp; using ITcpListenerConfig = IListenerConfig; -public sealed record TcpListener : ListenerConfig, ITcpListenerConfig +public sealed record TcpListener : ServerTransport, ITcpListenerConfig { public required IPEndPoint EndPoint { get; init; } diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs index 95911151..0e8b1552 100644 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs @@ -2,7 +2,7 @@ using IWebSocketListenerConfig = IListenerConfig; -public sealed record WebSocketListener : ListenerConfig, IWebSocketListenerConfig +public sealed record WebSocketListener : ServerTransport, IWebSocketListenerConfig { public required Accept Accept { get; init; } diff --git a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs index ff78f64a..a9e6d712 100644 --- a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs +++ b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs @@ -5,7 +5,7 @@ namespace UiPath.Ipc; -public interface ISerializer +internal interface ISerializer { ValueTask DeserializeAsync(Stream json, ILogger? logger); void Serialize(object? obj, Stream stream); diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.Ipc.Tests/ComputingTests.cs index a58e6172..24fdacb1 100644 --- a/src/UiPath.Ipc.Tests/ComputingTests.cs +++ b/src/UiPath.Ipc.Tests/ComputingTests.cs @@ -44,7 +44,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingletonAlias() ; - protected override ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) => listener with { ConcurrentAccepts = 10, @@ -279,12 +279,12 @@ await Enumerable.Range(1, CParallelism) .WhenAll(); } - public abstract IAsyncDisposable? RandomTransportPair(out ListenerConfig listener, out ClientTransport transport); + 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) { - public IAsyncDisposable? CreateListenerConfig(out ListenerConfig listenerConfig) + public IAsyncDisposable? CreateListenerConfig(out ServerTransport listenerConfig) { switch (Kind) { @@ -321,7 +321,7 @@ public enum ServerKind { NamedPipes, Tcp, WebSockets } private sealed class DisableInProcClientServer : OverrideConfig { - public override async Task Override(Func> listener) => null; + public override async Task Override(Func> listener) => null; public override IpcClient? Override(Func client) => null; } } diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs index 68235bae..0a6c67af 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs @@ -9,7 +9,7 @@ public sealed class ComputingTestsOverNamedPipes : ComputingTests public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeListener + protected override async Task CreateListener() => new NamedPipeListener { PipeName = PipeName }; @@ -19,7 +19,7 @@ public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outpu AllowImpersonation = true, }; - public override IAsyncDisposable? RandomTransportPair(out ListenerConfig listener, out ClientTransport transport) + public override IAsyncDisposable? RandomTransportPair(out ServerTransport listener, out ClientTransport transport) { var pipeName = $"{Guid.NewGuid():N}"; listener = new NamedPipeListener { PipeName = pipeName }; diff --git a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs index a49f2aa9..237c1e40 100644 --- a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs +++ b/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs @@ -34,6 +34,6 @@ public OverrideConfigAttribute(Type overrideConfigType) public abstract class OverrideConfig { - public virtual Task Override(Func> listener) => listener()!; + public virtual Task Override(Func> listener) => listener()!; public virtual IpcClient? Override(Func client) => client(); } \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs index 87f90be6..665dbab2 100644 --- a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs +++ b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs @@ -26,7 +26,7 @@ public async Task NamedPipesShoulNotLeak() private static IpcServer CreateServer(string pipeName) => new IpcServer { - Listeners = [ + Transport = [ new NamedPipeListener { PipeName = pipeName, diff --git a/src/UiPath.Ipc.Tests/Program.cs b/src/UiPath.Ipc.Tests/Program.cs index 6e917ad3..084096ce 100644 --- a/src/UiPath.Ipc.Tests/Program.cs +++ b/src/UiPath.Ipc.Tests/Program.cs @@ -25,7 +25,7 @@ { { typeof(IComputingService) }, }, - Listeners = [listener], + Transport = [listener], }; ipcServer.Start(); await ipcServer.WaitForStop(); diff --git a/src/UiPath.Ipc.Tests/RobotTests.cs b/src/UiPath.Ipc.Tests/RobotTests.cs index f632f26c..c07902d8 100644 --- a/src/UiPath.Ipc.Tests/RobotTests.cs +++ b/src/UiPath.Ipc.Tests/RobotTests.cs @@ -31,7 +31,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingleton() .AddSingletonAlias(); - protected override ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) => listener with { ConcurrentAccepts = 10, diff --git a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs index 78d607a0..22ef42cb 100644 --- a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs @@ -12,7 +12,7 @@ public sealed class RobotTestsOverNamedPipes : RobotTests public RobotTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeListener + protected override async Task CreateListener() => new NamedPipeListener { PipeName = PipeName }; diff --git a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs index 416f2fb5..afc79a1d 100644 --- a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs +++ b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs @@ -47,7 +47,7 @@ public async Task RemoteCallingSyncOverAsync_IpcShouldBeResilient(ScenarioId sce private static IpcServer CreateServer(string pipeName) => new IpcServer { - Listeners = [ + Transport = [ new NamedPipeListener { PipeName = pipeName, diff --git a/src/UiPath.Ipc.Tests/SystemTests.cs b/src/UiPath.Ipc.Tests/SystemTests.cs index a00f97a6..73dc59a3 100644 --- a/src/UiPath.Ipc.Tests/SystemTests.cs +++ b/src/UiPath.Ipc.Tests/SystemTests.cs @@ -30,7 +30,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingleton() .AddSingletonAlias(); - protected override ListenerConfig ConfigTransportAgnostic(ListenerConfig listener) + protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) => listener with { ConcurrentAccepts = 10, @@ -94,20 +94,20 @@ public async Task ClientWaitingForTooLongACall_ShouldThrowTimeout() private sealed class ServerExecutingTooLongACall_ShouldThrowTimeout_Config : OverrideConfig { - public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeouts.Short }; + public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeouts.Short }; public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeout.InfiniteTimeSpan); } private sealed class ClientWaitingForTooLongACall_ShouldThrowTimeout_Config : OverrideConfig { - public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeout.InfiniteTimeSpan }; + public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeout.InfiniteTimeSpan }; public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeouts.IpcRoundtrip); } - private ListenerConfig ShortClientTimeout(ListenerConfig listener) => listener with { RequestTimeout = TimeSpan.FromMilliseconds(100) }; - private ListenerConfig InfiniteServerTimeout(ListenerConfig listener) => listener with { RequestTimeout = Timeout.InfiniteTimeSpan }; + private ServerTransport ShortClientTimeout(ServerTransport listener) => listener with { RequestTimeout = TimeSpan.FromMilliseconds(100) }; + private ServerTransport InfiniteServerTimeout(ServerTransport listener) => listener with { RequestTimeout = Timeout.InfiniteTimeSpan }; [Fact] public async Task FireAndForget_ShouldWork() diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs index ebcdac6c..5d173821 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs @@ -9,7 +9,7 @@ public sealed class SystemTestsOverNamedPipes : SystemTests public SystemTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected sealed override async Task CreateListener() => new NamedPipeListener + protected sealed override async Task CreateListener() => new NamedPipeListener { PipeName = PipeName }; diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs index d1fefa1f..97432053 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs @@ -10,7 +10,7 @@ public sealed class SystemTestsOverTcp : SystemTests public SystemTestsOverTcp(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected sealed override async Task CreateListener() + protected sealed override async Task CreateListener() => new TcpListener { EndPoint = _endPoint, diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs index 09598cf6..235f0518 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs @@ -15,7 +15,7 @@ protected override async Task DisposeAsync() await base.DisposeAsync(); } - protected override async Task CreateListener() + protected override async Task CreateListener() { var listener = new WebSocketListener { diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.Ipc.Tests/TestBase.cs index 783bf6fa..3086c9b3 100644 --- a/src/UiPath.Ipc.Tests/TestBase.cs +++ b/src/UiPath.Ipc.Tests/TestBase.cs @@ -67,7 +67,7 @@ public TestBase(ITestOutputHelper outputHelper) protected abstract void ConfigureSpecificServices(IServiceCollection services); - private Task CreateListenerAndConfigure() + private Task CreateListenerAndConfigure() { var factory = async () => { @@ -101,7 +101,7 @@ public TestBase(ITestOutputHelper outputHelper) } } }, - Listeners = [listener], + Transport = [listener], ServiceProvider = _serviceProvider, Scheduler = GuiScheduler }; @@ -133,12 +133,12 @@ public TestBase(ITestOutputHelper outputHelper) protected void CreateLazyProxy(out Lazy lazy) where TContract : class => lazy = new(GetProxy); - protected abstract Task CreateListener(); + protected abstract Task CreateListener(); protected abstract ClientConfig CreateClientConfig(EndpointCollection? callbacks = null); protected abstract ClientTransport CreateClientTransport(); - protected abstract ListenerConfig ConfigTransportAgnostic(ListenerConfig listener); + protected abstract ServerTransport ConfigTransportAgnostic(ServerTransport listener); protected virtual async Task DisposeAsync() { From 9d862270811f05099589406675aec427a1bf9a72 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 25 Nov 2024 09:36:32 +0100 Subject: [PATCH 03/26] taking shape --- src/Playground/Playground.csproj | 1 + src/Playground/Program.cs | 31 +-- src/UiPath.CoreIpc/Client/ServiceClient.cs | 18 +- src/UiPath.CoreIpc/Config/ClientConfig.cs | 5 +- src/UiPath.CoreIpc/Config/IListenerConfig.cs | 1 - .../Config/IServiceClientConfig.cs | 2 +- src/UiPath.CoreIpc/Config/IpcServer.cs | 172 +++++++++++--- src/UiPath.CoreIpc/Config/Peer.cs | 2 - src/UiPath.CoreIpc/Config/ServerTransport.cs | 65 ++++-- src/UiPath.CoreIpc/Connection.cs | 24 +- .../Helpers/DefaultsExtensions.cs | 4 +- src/UiPath.CoreIpc/Helpers/Router.cs | 8 +- .../Polyfills/MemberNotNullAttribute.cs | 71 ++++++ .../Polyfills/MemberNotNullWhenAttribute.cs | 85 +++---- src/UiPath.CoreIpc/Server/IClient.cs | 7 + src/UiPath.CoreIpc/Server/Listener.cs | 217 ------------------ src/UiPath.CoreIpc/Server/Server.cs | 5 +- src/UiPath.CoreIpc/Server/ServerConnection.cs | 169 ++++---------- .../Server/ServerTransportRunner.cs | 10 + ...ansport.cs => NamedPipeClientTransport.cs} | 4 +- .../Transport/NamedPipe/NamedPipeListener.cs | 72 ------ .../NamedPipe/NamedPipeServerTransport.cs | 80 +++++++ .../Transport/Tcp/TcpListener.cs | 61 ----- .../Transport/Tcp/TcpServerTransport.cs | 56 +++++ .../Transport/WebSocket/WebSocketListener.cs | 38 --- .../WebSocket/WebSocketServerTransport.cs | 25 ++ src/UiPath.CoreIpc/Wire/Dtos.cs | 4 +- src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs | 10 +- src/UiPath.Ipc.Tests/ComputingTests.cs | 12 +- .../ComputingTestsOverNamedPipes.cs | 8 +- src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs | 4 +- .../RobotTestsOverNamedPipes.cs | 10 +- src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs | 4 +- .../SystemTestsOverNamedPipes.cs | 4 +- src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs | 2 +- .../SystemTestsOverWebSockets.cs | 2 +- 36 files changed, 582 insertions(+), 711 deletions(-) create mode 100644 src/UiPath.CoreIpc/Polyfills/MemberNotNullAttribute.cs create mode 100644 src/UiPath.CoreIpc/Server/IClient.cs delete mode 100644 src/UiPath.CoreIpc/Server/Listener.cs create mode 100644 src/UiPath.CoreIpc/Server/ServerTransportRunner.cs rename src/UiPath.CoreIpc/Transport/NamedPipe/{NamedPipeTransport.cs => NamedPipeClientTransport.cs} (89%) delete mode 100644 src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs delete mode 100644 src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs delete mode 100644 src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs create mode 100644 src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs diff --git a/src/Playground/Playground.csproj b/src/Playground/Playground.csproj index 760afbc7..334c100b 100644 --- a/src/Playground/Playground.csproj +++ b/src/Playground/Playground.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + 1998 diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index df10be22..613e8d5a 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -38,6 +38,7 @@ private static async Task Main(string[] args) { Scheduler = serverScheduler, ServiceProvider = serverSP, + RequestTimeout = TimeSpan.FromHours(10), Endpoints = new() { typeof(Contracts.IServerOperations), // DEVINE @@ -50,23 +51,15 @@ private static async Task Main(string[] args) }, typeof(Contracts.IClientOperations2) }, - Transport = [ - new NamedPipeListener() + Transport = new NamedPipeServerTransport() + { + PipeName = Contracts.PipeName, + ServerName = ".", + AccessControl = ps => { - PipeName = Contracts.PipeName, - ServerName = ".", - AccessControl = ps => - { - }, - MaxReceivedMessageSizeInMegabytes = 100, - RequestTimeout = TimeSpan.FromHours(10) }, - //new BidirectionalHttp.ListenerConfig() - //{ - // Uri = serverUri, - // RequestTimeout = TimeSpan.FromHours(1) - //} - ] + MaxReceivedMessageSizeInMegabytes = 100, + } }; try @@ -91,9 +84,9 @@ private static async Task Main(string[] args) { typeof(Contracts.IClientOperations2), new Impl.Client2() }, }, ServiceProvider = clientSP, - Scheduler = clientScheduler, + Scheduler = clientScheduler, }, - Transport = new NamedPipeTransport() + Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, ServerName = ".", @@ -113,7 +106,7 @@ private static async Task Main(string[] args) }, Scheduler = clientScheduler, }, - Transport = new NamedPipeTransport() + Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, ServerName = ".", @@ -133,7 +126,7 @@ private static async Task Main(string[] args) }, Scheduler = clientScheduler, }, - Transport = new NamedPipeTransport() + Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, ServerName = ".", diff --git a/src/UiPath.CoreIpc/Client/ServiceClient.cs b/src/UiPath.CoreIpc/Client/ServiceClient.cs index dfe12f2b..4c0edd2f 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -93,7 +93,7 @@ async Task Invoke() throw; } - return response.Deserialize(Config.Serializer); + return response.Deserialize(); } catch (Exception ex) { @@ -123,7 +123,7 @@ string[] SerializeArguments() break; } - result[index] = Config.Serializer.OrDefault().Serialize(args[index]); + result[index] = IpcJsonSerializer.Instance.Serialize(args[index]); } return result; @@ -227,7 +227,7 @@ public override async ValueTask CloseConnection() var network = await Connect(ct); - LatestConnection = new Connection(network, Config.Serializer, Config.Logger, Config.DebugName); + LatestConnection = new Connection(network, Config.DebugName, Config.Logger); var router = new Router(_client.Config.CreateCallbackRouterConfig(), _client.Config.ServiceProvider); _latestServer = new Server(router, _client.Config.RequestTimeout, LatestConnection); @@ -263,19 +263,21 @@ private async Task Connect(CancellationToken ct) protected override IServiceClientConfig Config => _client.Config; } -internal sealed class ServiceClientForCallback : ServiceClient +internal sealed class ServiceClientForCallback : ServiceClient where TInterface : class { private readonly Connection _connection; - private readonly Listener _listener; + private readonly IServiceClientConfig _config; public override Stream? Network => _connection.Network; - public ServiceClientForCallback(Connection connection, Listener listener, Type interfaceType) : base(interfaceType) + public ServiceClientForCallback(Connection connection, IServiceClientConfig config) : base(typeof(TInterface)) { _connection = connection; - _listener = listener; + _config = config; } + public TInterface GetProxy() => GetProxy(); + public override void Dispose() { // do nothing @@ -284,5 +286,5 @@ public override void Dispose() protected override Task<(Connection connection, bool newlyConnected)> EnsureConnection(CancellationToken ct) => Task.FromResult((_connection, newlyConnected: false)); - protected override IServiceClientConfig Config => _listener.Config; + protected override IServiceClientConfig Config => _config; } diff --git a/src/UiPath.CoreIpc/Config/ClientConfig.cs b/src/UiPath.CoreIpc/Config/ClientConfig.cs index 7d77abfa..c6ac4bdb 100644 --- a/src/UiPath.CoreIpc/Config/ClientConfig.cs +++ b/src/UiPath.CoreIpc/Config/ClientConfig.cs @@ -2,16 +2,13 @@ namespace UiPath.Ipc; -public sealed record ClientConfig : Peer, IServiceClientConfig +public sealed class ClientConfig : Peer, IServiceClientConfig { public EndpointCollection? Callbacks { get; init; } - public IServiceProvider? ServiceProvider { get; init; } public ILogger? Logger { get; init; } public BeforeConnectHandler? BeforeConnect { get; init; } public BeforeCallHandler? BeforeCall { get; init; } - public TaskScheduler? Scheduler { get; init; } - public ISerializer? Serializer { get; set; } [EditorBrowsable(EditorBrowsableState.Never)] public string DebugName { get; set; } = null!; diff --git a/src/UiPath.CoreIpc/Config/IListenerConfig.cs b/src/UiPath.CoreIpc/Config/IListenerConfig.cs index dd8976cc..da4732fc 100644 --- a/src/UiPath.CoreIpc/Config/IListenerConfig.cs +++ b/src/UiPath.CoreIpc/Config/IListenerConfig.cs @@ -9,4 +9,3 @@ internal interface IListenerConfig ValueTask AwaitConnection(TListenerState listenerState, TConnectionState connectionState, CancellationToken ct); IEnumerable Validate(); } - diff --git a/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs b/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs index 95d3a29e..19ed49fd 100644 --- a/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs +++ b/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs @@ -1,11 +1,11 @@ namespace UiPath.Ipc; +// Maybe decommission internal interface IServiceClientConfig { TimeSpan RequestTimeout { get; } BeforeConnectHandler? BeforeConnect { get; } BeforeCallHandler? BeforeCall { get; } ILogger? Logger { get; } - ISerializer? Serializer { get; } string DebugName { get; } } diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index a9485a3b..d0f58020 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -1,64 +1,174 @@ -namespace UiPath.Ipc; +using System.Diagnostics.CodeAnalysis; + +namespace UiPath.Ipc; public sealed class IpcServer : Peer, IAsyncDisposable { public required EndpointCollection Endpoints { get; init; } public required ServerTransport Transport { get; init; } - private readonly Lazy _listener; - private readonly TaskCompletionSource _tcsStopped = new(); + private readonly object _lock = new(); + private readonly TaskCompletionSource _listening = new(); + private readonly TaskCompletionSource _stopped = new(); + private readonly CancellationTokenSource _ctsActiveConnections = new(); + + private bool _disposeStarted; + private Accepter? _accepter; - public IpcServer() + public async ValueTask DisposeAsync() { - _listener = new(() => Listener.Create(server: this)); + Accepter? accepter = null; + lock (_lock) + { + _disposeStarted = true; + accepter = _accepter; + } + + await (accepter?.DisposeAsync() ?? default); + _ctsActiveConnections.Cancel(); + _ctsActiveConnections.Dispose(); } + [MemberNotNull(nameof(Transport), nameof(_accepter))] public void Start() { - if (!IsValid(out var errors)) + lock (_lock) { - throw new InvalidOperationException($"ValidationErrors:\r\n{string.Join("\r\n", errors)}"); + 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; + } + public Task WaitForStop() => _stopped.Task; + + 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}"); + _stopped.TrySetException(ex); + } + + private sealed class ObserverAdapter : IObserver + { + public required Action OnNext { get; init; } + public Action? OnError { get; init; } + public Action? OnCompleted { get; init; } - _ = _started.Value; + void IObserver.OnNext(T value) => OnNext(value); + void IObserver.OnError(Exception error) => OnError?.Invoke(error); + void IObserver.OnCompleted() => OnCompleted?.Invoke(); } - public Task WaitForStart() => _started.Value; - public Task WaitForStop() => _tcsStopped.Task; - private Listener? StartCore() + private sealed class Accepter : IAsyncDisposable { - if (!IsValid(out _)) + private readonly CancellationTokenSource _cts = new(); + private readonly ServerTransport.IServerState _serverState; + private readonly Task _running; + private readonly IObserver _newConnection; + private readonly TaskCompletionSource _tcsStartedAccepting = new(); + + public Task StartedAccepting => _tcsStartedAccepting.Task; + + public Accepter(ServerTransport transport, IObserver connected) { - return null; + _serverState = transport.CreateServerState(); + _newConnection = connected; + _running = RunOnThreadPool(LoopAccept, parallelCount: transport.ConcurrentAccepts, _cts.Token); } - try + public async ValueTask DisposeAsync() { - return Listener.Create(this, Transport); + _cts.Cancel(); + await _running; + _cts.Dispose(); } - catch (Exception ex) + + private async Task LoopAccept(CancellationToken ct) { - Trace.TraceError($"Failed to start server. Ex: {ex}"); - _tcsStopped.TrySetException(ex); - disposables.SetException(ex); - await disposables.DisposeAsync(); - throw; + try + { + while (!ct.IsCancellationRequested) + { + await Accept(ct); + } + } + catch (OperationCanceledException ex) when (ex.CancellationToken == ct) + { + // Ignore + } + + _newConnection.OnCompleted(); } - } - private bool IsValid(out IReadOnlyList errors) - { - errors = PrefixErrors(Transport).ToArray(); - return errors is { Count: 0 }; + private async Task Accept(CancellationToken ct) + { + var slot = _serverState.CreateConnectionSlot(); - static IEnumerable PrefixErrors(ServerTransport transport) - => transport.Validate().Select(error => $"{transport.GetType().Name}: {error}"); + try + { + var taskNewConnection = slot.AwaitConnection(ct); + _tcsStartedAccepting.TrySetResult(null); + + var newConnection = await taskNewConnection; + _newConnection.OnNext(newConnection); + } + catch (Exception ex) + { + slot.Dispose(); + _newConnection.OnError(ex); + return; + } + } + + private static Task RunOnThreadPool(Func action, int parallelCount, CancellationToken ct) + => Task.WhenAll(Enumerable.Range(start: 0, parallelCount).Select(_ => Task.Run(() => action(ct)))); } - public async ValueTask DisposeAsync() + [MemberNotNullWhen(returnValue: true, member: nameof(Transport))] + private bool IsValid([NotNullWhen(returnValue: false)] out string? errorMessage) { - var maybeLogger = ServiceProvider.GetService()?.CreateLogger(typeof(IpcServer)); + 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; + } - await ((await _started.Value)?.DisposeAsync() ?? default); + errorMessage = null; + return true; } } diff --git a/src/UiPath.CoreIpc/Config/Peer.cs b/src/UiPath.CoreIpc/Config/Peer.cs index 432e3a86..c7e9ba8a 100644 --- a/src/UiPath.CoreIpc/Config/Peer.cs +++ b/src/UiPath.CoreIpc/Config/Peer.cs @@ -6,8 +6,6 @@ public abstract class Peer public IServiceProvider? ServiceProvider { get; init; } public TaskScheduler? Scheduler { get; init; } - internal ISerializer? Serializer => IpcJsonSerializer.Instance; - internal virtual RouterConfig CreateRouterConfig(IpcServer server) => throw new NotSupportedException(); internal virtual RouterConfig CreateCallbackRouterConfig() => throw new NotSupportedException(); diff --git a/src/UiPath.CoreIpc/Config/ServerTransport.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs index 4c15ec53..ed49685e 100644 --- a/src/UiPath.CoreIpc/Config/ServerTransport.cs +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -1,31 +1,58 @@ -using System.Security.Cryptography.X509Certificates; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; namespace UiPath.Ipc; -public abstract class ServerTransport : Peer, IServiceClientConfig +public abstract class ServerTransport { public int ConcurrentAccepts { get; init; } = 5; public byte MaxReceivedMessageSizeInMegabytes { get; init; } = 2; public X509Certificate? Certificate { get; init; } internal int MaxMessageSize => MaxReceivedMessageSizeInMegabytes * 1024 * 1024; - internal IEnumerable Validate() => Enumerable.Empty(); + // TODO: Maybe decommission. + internal async Task MaybeAuthenticate(Stream network) + { + if (Certificate is null) + { + return network; + } - internal override RouterConfig CreateRouterConfig(IpcServer server) - => RouterConfig.From( - server.Endpoints, - endpoint => endpoint with + var sslStream = new SslStream(network, leaveInnerStreamOpen: false); + try + { + await sslStream.AuthenticateAsServerAsync(Certificate); + } + catch { - Scheduler = endpoint.Scheduler ?? server.Scheduler - }); - - #region IServiceClientConfig - /// Do not implement explicitly, as it must be implicitly implemented by . - - BeforeConnectHandler? IServiceClientConfig.BeforeConnect => null; - BeforeCallHandler? IServiceClientConfig.BeforeCall => null; - ILogger? IServiceClientConfig.Logger => null; - ISerializer? IServiceClientConfig.Serializer => null!; - string IServiceClientConfig.DebugName => $"CallbackClient for {this}"; - #endregion + sslStream.Dispose(); + throw; + } + + Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); + return sslStream; + } + + protected internal abstract IServerState CreateServerState(); + + internal IEnumerable Validate() + => ValidateCore().Where(x => x is not null).Select(x => $"{GetType().Name}.{x}"); + protected abstract IEnumerable ValidateCore(); + protected static string? IsNotNull(T? propertyValue, [CallerArgumentExpression(nameof(propertyValue))] string? propertyName = null) + { + if (propertyValue is null) + { + return $"{propertyName} is required."; + } + return null; + } + + protected internal interface IServerState : IAsyncDisposable + { + IServerConnectionSlot CreateConnectionSlot(); + } + protected internal interface IServerConnectionSlot : IDisposable + { + ValueTask AwaitConnection(CancellationToken ct); + } } diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index a8c3aa30..be2b3bde 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipes; namespace UiPath.Ipc; @@ -21,22 +20,21 @@ internal sealed class Connection : IDisposable private readonly Action _cancelRequest; private readonly byte[] _buffer = new byte[sizeof(long)]; private readonly NestedStream _nestedStream; + + public string DebugName { get; } + public ILogger? Logger { get; } + public Stream Network { get; } - public ILogger? Logger { get; internal set; } [MemberNotNullWhen(returnValue: true, nameof(Logger))] public bool LogEnabled => Logger.Enabled(); - public string DebugName { get; } - public ISerializer? Serializer { get; } - - public Connection(Stream network, ISerializer? serializer, ILogger? logger, string debugName, int maxMessageSize = int.MaxValue) + 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; - DebugName = $"{debugName} {GetHashCode()}"; _maxMessageSize = maxMessageSize; _onResponse = response => OnResponseReceived((Response)response!); _onRequest = request => OnRequestReceived((Request)request!); @@ -49,8 +47,8 @@ public Connection(Stream network, ISerializer? serializer, ILogger? logger, stri public string NewRequestId() => Interlocked.Increment(ref _requestCounter).ToString(); internal Task Listen() => _receiveLoop.Value; - internal event Func RequestReceived; - internal event Action CancellationReceived; + internal event Func? RequestReceived; + internal event Action? CancellationReceived; public event EventHandler? Closed; #if !NET461 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] @@ -351,7 +349,7 @@ private MemoryStream SerializeToStream(object value) try { stream.Position = HeaderLength; - Serializer.OrDefault().Serialize(value, stream); + IpcJsonSerializer.Instance.Serialize(value, stream); return stream; } catch @@ -360,7 +358,7 @@ private MemoryStream SerializeToStream(object value) throw; } } - private ValueTask Deserialize() => Serializer.OrDefault().DeserializeAsync(_nestedStream, Logger); + private ValueTask Deserialize() => IpcJsonSerializer.Instance.DeserializeAsync(_nestedStream, Logger); private void OnCancellationReceived(CancellationRequest cancellationRequest) { diff --git a/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs index 9752811d..4b6d9ca1 100644 --- a/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs +++ b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs @@ -4,8 +4,8 @@ namespace UiPath.Ipc; internal static class DefaultsExtensions { - public static ISerializer OrDefault(this ISerializer? serializer) => serializer ?? IpcJsonSerializer.Instance; - public static ILoggerFactory OrDefault(this ILoggerFactory? loggerFactory) => loggerFactory ?? NullLoggerFactory.Instance; + 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; diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs index ff3d25a6..45252243 100644 --- a/src/UiPath.CoreIpc/Helpers/Router.cs +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -24,6 +24,12 @@ 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; @@ -128,7 +134,6 @@ public static Route From(IServiceProvider? serviceProvider, EndpointSettings end BeforeCall = endpointSettings.BeforeCall, Scheduler = endpointSettings.Scheduler.OrDefault(), LoggerFactory = serviceProvider.MaybeCreateServiceFactory(), - Serializer = serviceProvider.MaybeCreateServiceFactory() }; public required ServiceFactory Service { get; init; } @@ -136,5 +141,4 @@ public static Route From(IServiceProvider? serviceProvider, EndpointSettings end public TaskScheduler Scheduler { get; init; } public BeforeCallHandler? BeforeCall { get; init; } public Func? LoggerFactory { get; init; } - public Func? Serializer { get; init; } } 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 index bf9ad270..b49daaab 100644 --- a/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs +++ b/src/UiPath.CoreIpc/Polyfills/MemberNotNullWhenAttribute.cs @@ -5,67 +5,34 @@ 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 +// +// 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 { - /// - /// Gets the return value condition. - /// - public bool ReturnValue { get; } - - /// - /// Gets field or property member names. - /// + // + // 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; } - - /// - /// 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/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 df472011..00000000 --- a/src/UiPath.CoreIpc/Server/Listener.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System.Linq.Expressions; - -namespace UiPath.Ipc; - -using ListenerFactory = Func; - -internal abstract class Listener : IAsyncDisposable -{ - private static readonly GenericListenerFactoryCache Cache = new(); - - public static Listener Create(IpcServer server) - { - var transportType = server.Transport.GetType(); - var listenerFactory = Cache.Get(transportType); - var listener = listenerFactory(server); - return listener; - } - - private readonly Lazy _disposeTask = null!; - public readonly ServerTransport Config; - public readonly IpcServer Server; - private readonly Lazy _loggerCategory; - - private readonly Lazy _lazyLogger; - public ILogger Logger => _lazyLogger.Value; - - protected Listener(IpcServer server, ServerTransport config) - { - _loggerCategory = new(ComputeLoggerCategory); - Config = config; - Server = server; - _lazyLogger = new(() => server.ServiceProvider.GetService().OrDefault().CreateLogger(LoggerCategory)); - _disposeTask = new(DisposeCore); - } - - ValueTask IAsyncDisposable.DisposeAsync() => new(_disposeTask.Value); - - protected abstract Task DisposeCore(); - - private string LoggerCategory => _loggerCategory.Value; - - private string ComputeLoggerCategory() - => $"{GetType().Namespace}.{nameof(Listener)}<{ConfigType.Name}[{Config}],..>"; - - protected abstract Type ConfigType { get; } -} - -internal sealed class Listener : Listener, IAsyncDisposable - where TConfig : ServerTransport, IListenerConfig - where TListenerState : IAsyncDisposable - where TConnectionState : IDisposable -{ - private readonly CancellationTokenSource _cts = new(); - private readonly Task _listeningTask = null!; - - public new readonly TConfig Config; - - public TListenerState State { get; } - - public Listener(IpcServer server, TConfig config) : base(server, config) - { - Config = config; - State = Config.CreateListenerState(server); - - _listeningTask = Task.Run(() => Listen(_cts.Token)); - } - - public void Log(string message) - { - if (!Logger.Enabled()) - { - return; - } - - Logger.LogInformation(message); - } - public void LogError(Exception exception, string message) - { - if (!Logger.Enabled(LogLevel.Error)) - { - return; - } - - Logger.LogError(exception, message); - } - - protected override async Task DisposeCore() - { - Log($"Stopping listener {Config}..."); - try - { - _cts.Cancel(); - } - catch (Exception ex) - { - LogError(ex, $"Canceling {Config} failed."); - } - try - { - await _listeningTask; - } - catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token) - { - Log($"Stopping listener {Config} threw OCE."); - } - catch (Exception ex) - { - LogError(ex, $"Stopping listener {Config} failed."); - } - await State.DisposeAsync(); - _cts.Dispose(); - } - - private async Task Listen(CancellationToken ct) - { - Log($"Starting listener {Config}..."); - - await Task.WhenAll(Enumerable.Range(1, Config.ConcurrentAccepts).Select(async _ => - { - while (!ct.IsCancellationRequested) - { - await AcceptConnection(ct); - } - })); - } - private async Task AcceptConnection(CancellationToken ct) - { - var serverConnection = new ServerConnection(this); - - Stream? network = null; - try - { - network = await serverConnection.AcceptClient(ct); - } - catch - { - serverConnection.Dispose(); - return; - } - - try - { - _ = Task.Run(TryToListen); - } - catch (Exception ex) - { - serverConnection.Dispose(); - if (!ct.IsCancellationRequested) - { - Logger.LogException(ex, Config); - } - } - - async Task TryToListen() - { - try - { - await serverConnection.Listen(network, ct); - } - catch (Exception ex) - { - Logger.LogException(ex, $"Listen loop failed for {Config}"); - } - } - } - - protected override Type ConfigType => typeof(TConfig); -} - -internal sealed class GenericListenerFactoryCache -{ - private readonly ConcurrentDictionary> _cache = new(); - - public ListenerFactory Get(Type configType) => _cache.GetOrAdd(configType, Create).Value; - - private Result Create(Type configType) - { - try - { - return new(Emit(configType)); - } - catch (Exception ex) - { - return new(ex); - } - - static ListenerFactory Emit(Type configType) - { - if (configType.GetInterfaces().SingleOrDefault(IsIListenerConfig) is not { } iface - || iface.GetGenericArguments() is not [_, var listenerStateType, var connectionStateType]) - { - throw new ArgumentOutOfRangeException(nameof(iface), $"The ListenerConfig type must implement IListenerConfig<,>. ListenerConfig type was: {configType.FullName}"); - } - - var listenerType = typeof(Listener<,,>).MakeGenericType(configType, listenerStateType, connectionStateType); - var listenerCtor = listenerType.GetConstructor( - bindingAttr: BindingFlags.Public | BindingFlags.Instance, - binder: Type.DefaultBinder, - types: [typeof(IpcServer), configType], - modifiers: null)!; - - var paramofServer = Expression.Parameter(typeof(IpcServer)); - var paramofConfig = Expression.Parameter(typeof(ServerTransport)); - var lambda = Expression.Lambda( - delegateType: typeof(ListenerFactory), - body: Expression.New(listenerCtor, paramofServer, Expression.Convert(paramofConfig, configType)), - paramofServer, - paramofConfig); - - var @delegate = (lambda.Compile() as ListenerFactory)!; - return @delegate; - } - - static bool IsIListenerConfig(Type candidateIface) - => candidateIface.IsGenericType && candidateIface.GetGenericTypeDefinition() == typeof(IListenerConfig<,,>); - } -} diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index e54c6c96..16b1e691 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -28,7 +28,6 @@ static Server() private ILogger? Logger => _connection.Logger; private bool LogEnabled => Logger.Enabled(); - public ISerializer Serializer => _connection.Serializer.OrDefault(); public string DebugName => _connection.DebugName; public Server(Router router, TimeSpan requestTimeout, Connection connection, IClient? client = null) @@ -154,7 +153,7 @@ async ValueTask InvokeMethod() return result switch { Stream downloadStream => Response.Success(request, downloadStream), - var x => Response.Success(request, Serializer.Serialize(x)) + var x => Response.Success(request, IpcJsonSerializer.Instance.Serialize(x)) }; } @@ -213,7 +212,7 @@ void Deserialize() } else { - argument = Serializer.Deserialize(request.Parameters[index], parameterType); + argument = IpcJsonSerializer.Instance.Deserialize(request.Parameters[index], parameterType); argument = CheckMessage(argument, parameterType); } allArguments[index] = argument; diff --git a/src/UiPath.CoreIpc/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index be2c90c8..cde20546 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -1,155 +1,80 @@ using System.IO.Pipes; -using System.Net.Security; -namespace UiPath.Ipc; -public interface IClient -{ - TCallbackInterface GetCallback() where TCallbackInterface : class; - void Impersonate(Action action); -} +namespace UiPath.Ipc; -internal sealed class ServerConnection : ServerConnection - where TConfig : ServerTransport, IListenerConfig - where TListenerState : IAsyncDisposable - where TConnectionState : IDisposable +internal sealed class ServerConnection : IClient, IDisposable, IServiceClientConfig { - public new readonly Listener Listener; - - private readonly object _lock = new(); - private bool _acceptClientCalled = false; - private bool _disposed = false; - private TConnectionState? _connectionState; - - public ServerConnection(Listener listener) : base(listener) => Listener = listener; - - public override ValueTask AcceptClient(CancellationToken ct) + public static void CreateAndListen(IpcServer server, Stream network, CancellationToken ct) { - lock (_lock) + _ = Task.Run(async () => { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ServerConnection)); - } - if (_acceptClientCalled) - { - throw new InvalidOperationException("AcceptClient can only be called once."); - } - _acceptClientCalled = true; - _connectionState = Listener.Config.CreateConnectionState(Listener.Server, Listener.State); - } - - return Listener.Config.AwaitConnection(Listener.State, _connectionState, ct); + _ = new ServerConnection(server, await server.Transport.MaybeAuthenticate(network), ct); + }); } - public override void Dispose() - { - base.Dispose(); + private readonly string _debugName; + private readonly ILogger? _logger; + private readonly ConcurrentDictionary _callbacks = new(); + private readonly IpcServer _ipcServer; - lock (_lock) - { - if (_disposed) - { - return; - } - _disposed = true; - - if (_connectionState is not null) - { - _connectionState.Dispose(); - } - } - } -} + private readonly Stream _network; + private readonly Connection _connection; + private readonly Server _server; -internal abstract class ServerConnection : IClient, IDisposable -{ - public readonly Listener Listener; + private readonly Task _listening; - private readonly ConcurrentDictionary _callbacks = new(); - internal Connection? Connection; - private Task? _connectionAsTask; - private Server? Server; + private ServerConnection(IpcServer server, Stream network, CancellationToken ct) + { + _ipcServer = server; - protected ServerConnection(Listener listener) => Listener = listener; + _debugName = $"{nameof(ServerConnection)} {RuntimeHelpers.GetHashCode(this)}"; + _logger = server.CreateLogger(_debugName); - protected internal virtual void Initialize() { } + _network = network; - public abstract ValueTask AcceptClient(CancellationToken cancellationToken); + _connection = new Connection(network, _debugName, _logger, maxMessageSize: _ipcServer.Transport.MaxMessageSize); + _server = new Server(new Router(_ipcServer), _ipcServer.RequestTimeout, _connection, client: this); - public void Impersonate(Action action) - { - if (Connection is null) - { - throw new InvalidOperationException("The server connection is not listening yet."); - } + _listening = Listen(ct); + } - if (Connection.Network is not NamedPipeServerStream pipeStream) + private async Task Listen(CancellationToken ct) + { + // close the connection when the service host closes + using (ct.UnsafeRegister(_ => _connection.Dispose(), state: null)) { - action(); - return; + await _connection.Listen(); } - - pipeStream.RunAsClient(() => action()); } - TCallbackInterface IClient.GetCallback() where TCallbackInterface : class + void IDisposable.Dispose() => _network.Dispose(); + + TCallbackInterface IClient.GetCallback() { return (TCallbackInterface)_callbacks.GetOrAdd(typeof(TCallbackInterface), CreateCallback); TCallbackInterface CreateCallback(Type callbackContract) { - Listener.Logger.LogInformation($"Create callback {callbackContract} {Listener.Config}"); - - _connectionAsTask ??= Task.FromResult(Connection!); - - // TODO: rethink this double specification of TCallbackInterface - return new ServiceClientForCallback(Connection!, Listener, typeof(TCallbackInterface)).GetProxy(); + _logger.LogInformation($"Create callback {callbackContract}."); + return new ServiceClientForCallback(_connection, config: this).GetProxy(); } } - public async Task Listen(Stream network, CancellationToken cancellationToken) + void IClient.Impersonate(Action action) { - var stream = await AuthenticateAsServer(); // TODO: should we decommission this? - var serializer = Listener.Server.ServiceProvider.GetService(); - - Connection = new Connection(stream, serializer, Listener.Logger, debugName: Listener.ToString()!, maxMessageSize: Listener.Config.MaxMessageSize); - - Server = new Server( - new Router( - Listener.Config.CreateRouterConfig(Listener.Server), - Listener.Server.ServiceProvider), - Listener.Config.RequestTimeout, - Connection, - client: this); - - // close the connection when the service host closes - using (cancellationToken.UnsafeRegister(_ => Connection.Dispose(), state: null)) + if (_connection.Network is not NamedPipeServerStream pipeStream) { - await Connection.Listen(); + action(); + return; } - return; - // TODO: should we decommission this? - async Task AuthenticateAsServer() - { - if (Listener.Config.Certificate is null) - { - return network; - } - - var sslStream = new SslStream(network); - try - { - await sslStream.AuthenticateAsServerAsync(Listener.Config.Certificate); - } - catch - { - sslStream.Dispose(); - throw; - } - - Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); - return sslStream; - } + pipeStream.RunAsClient(() => action()); } - public virtual void Dispose() { } + + #region IServiceClientConfig + TimeSpan IServiceClientConfig.RequestTimeout => _ipcServer.RequestTimeout; + BeforeConnectHandler? IServiceClientConfig.BeforeConnect => null; + BeforeCallHandler? IServiceClientConfig.BeforeCall => null; + ILogger? IServiceClientConfig.Logger => _logger; + string IServiceClientConfig.DebugName => _debugName; + #endregion } diff --git a/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs b/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs new file mode 100644 index 00000000..bb68f927 --- /dev/null +++ b/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs @@ -0,0 +1,10 @@ +namespace UiPath.Ipc; + +internal static class ServerTransportRunner +{ + public static async Task Start(ServerTransport transport) + { + var serverState = transport.CreateServerState(); + return serverState; + } +} diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs similarity index 89% rename from src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs rename to src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs index ba8fe11e..d6bd0aff 100644 --- a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeTransport.cs +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs @@ -3,7 +3,7 @@ namespace UiPath.Ipc.Transport.NamedPipe; -public sealed record NamedPipeTransport : ClientTransport +public sealed record NamedPipeClientTransport : ClientTransport { public required string PipeName { get; init; } public string ServerName { get; init; } = "."; @@ -35,7 +35,7 @@ internal sealed class NamedPipeClientState : IClientState public async ValueTask Connect(IpcClient client, CancellationToken ct) { - var transport = client.Transport as NamedPipeTransport ?? throw new InvalidOperationException(); + var transport = client.Transport as NamedPipeClientTransport ?? throw new InvalidOperationException(); _pipe = new NamedPipeClientStream( transport.ServerName, diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs deleted file mode 100644 index 219fc7a6..00000000 --- a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeListener.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Newtonsoft.Json; -using System.IO.Pipes; -using System.Security.Principal; - -namespace UiPath.Ipc.Transport.NamedPipe; - -using INamedPipeListenerConfig = IListenerConfig; - -public sealed record NamedPipeListener : ServerTransport, INamedPipeListenerConfig -{ - public required string PipeName { get; init; } - public string ServerName { get; init; } = "."; - [JsonIgnore] - public AccessControlDelegate? AccessControl { get; init; } - - private PipeSecurity? GetPipeSecurity() - { - var setAccessControl = AccessControl; - if (setAccessControl is 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); - } - - NamedPipeListenerState INamedPipeListenerConfig.CreateListenerState(IpcServer server) - => new(); - - NamedPipeServerConnectionState INamedPipeListenerConfig.CreateConnectionState(IpcServer server, NamedPipeListenerState listenerState) - => new() - { - Stream = IOHelpers.NewNamedPipeServerStream( - PipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - GetPipeSecurity) - }; - - async ValueTask INamedPipeListenerConfig.AwaitConnection(NamedPipeListenerState listenerState, NamedPipeServerConnectionState connectionState, CancellationToken ct) - { - await connectionState.Stream.WaitForConnectionAsync(ct); - return connectionState.Stream; - } - - IEnumerable INamedPipeListenerConfig.Validate() - { - if (PipeName is null or "") { yield return "PipeName is required"; } - } - - public override string ToString() => $"ServerPipe={PipeName}"; -} - -internal sealed class NamedPipeServerConnectionState : IDisposable -{ - public required NamedPipeServerStream Stream { get; init; } - - public void Dispose() => Stream.Dispose(); -} - -internal sealed class NamedPipeListenerState : IAsyncDisposable -{ - public ValueTask DisposeAsync() => default; -} diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs new file mode 100644 index 00000000..aff79351 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs @@ -0,0 +1,80 @@ +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; } + + protected internal override IServerState CreateServerState() + => new ServerState { Transport = this }; + + protected 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; } + + void IDisposable.Dispose() => Stream.Dispose(); + + async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + { + await Stream.WaitForConnectionAsync(ct); + return Stream; + } + } +} diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs deleted file mode 100644 index fc146f5a..00000000 --- a/src/UiPath.CoreIpc/Transport/Tcp/TcpListener.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace UiPath.Ipc.Transport.Tcp; - -using ITcpListenerConfig = IListenerConfig; - -public sealed record TcpListener : ServerTransport, ITcpListenerConfig -{ - public required IPEndPoint EndPoint { get; init; } - - TcpListenerState ITcpListenerConfig.CreateListenerState(IpcServer server) - { - var listener = new System.Net.Sockets.TcpListener(EndPoint); - listener.Start(backlog: ConcurrentAccepts); - - return new() { Listener = listener }; - } - - TcpServerConnectionState ITcpListenerConfig.CreateConnectionState(IpcServer server, TcpListenerState listenerState) - => new(); - - async ValueTask ITcpListenerConfig.AwaitConnection(TcpListenerState listenerState, TcpServerConnectionState connectionState, CancellationToken ct) - { - System.Net.Sockets.TcpClient tcpClient; -#if NET461 - using var ctreg = ct.Register(listenerState.Listener.Stop); - tcpClient = await listenerState.Listener.AcceptTcpClientAsync(); -#else - tcpClient = await listenerState.Listener.AcceptTcpClientAsync(ct); -#endif - return tcpClient.GetStream(); - } - - IEnumerable ITcpListenerConfig.Validate() - { - if (EndPoint is null) - { - yield return "EndPoint is required"; - } - } - - public override string ToString() => $"TcpServer={EndPoint}"; -} - -internal sealed class TcpListenerState : IAsyncDisposable -{ - public required System.Net.Sockets.TcpListener Listener { get; init; } - - public ValueTask DisposeAsync() - { - Listener.Stop(); - return default; - } -} - -internal sealed class TcpServerConnectionState : IDisposable -{ - public void 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..18bb0a5f --- /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; } + + protected internal override IServerState CreateServerState() + { + var listener = new TcpListener(EndPoint); + listener.Start(backlog: ConcurrentAccepts); + return new ServerState() { TcpListener = listener }; + } + + protected 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(); + } + + void IDisposable.Dispose() { } + } +} diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs deleted file mode 100644 index 0e8b1552..00000000 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketListener.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace UiPath.Ipc.Transport.WebSocket; - -using IWebSocketListenerConfig = IListenerConfig; - -public sealed record WebSocketListener : ServerTransport, IWebSocketListenerConfig -{ - public required Accept Accept { get; init; } - - WebSocketListenerState IWebSocketListenerConfig.CreateListenerState(IpcServer server) - => new(); - - WebSocketServerConnectionState IWebSocketListenerConfig.CreateConnectionState(IpcServer server, WebSocketListenerState listenerState) - => new(); - - async ValueTask IWebSocketListenerConfig.AwaitConnection(WebSocketListenerState listenerState, WebSocketServerConnectionState connectionState, CancellationToken ct) - { - var webSocket = await Accept(ct); - - return new WebSocketStream(webSocket); - } - - IEnumerable IWebSocketListenerConfig.Validate() - { - if (Accept is null) { yield return "Accept is required"; } - } - - public override string ToString() => "WebSocketServer"; -} - -internal sealed class WebSocketListenerState : IAsyncDisposable -{ - public ValueTask DisposeAsync() => default; -} - -internal sealed class WebSocketServerConnectionState : IDisposable -{ - public void 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..14110555 --- /dev/null +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs @@ -0,0 +1,25 @@ +namespace UiPath.Ipc.Transport.WebSocket; + +public sealed class WebSocketServerTransport : ServerTransport, ServerTransport.IServerState, ServerTransport.IServerConnectionSlot +{ + public required Accept Accept { get; init; } + + protected internal override IServerState CreateServerState() => this; + + IServerConnectionSlot IServerState.CreateConnectionSlot() => this; + + async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + { + var webSocket = await Accept(ct); + return new WebSocketStream(webSocket); + } + ValueTask IAsyncDisposable.DisposeAsync() => default; + void IDisposable.Dispose() { } + + protected override IEnumerable ValidateCore() + { + yield return IsNotNull(Accept); + } + + public override string ToString() => nameof(WebSocketServerTransport); +} diff --git a/src/UiPath.CoreIpc/Wire/Dtos.cs b/src/UiPath.CoreIpc/Wire/Dtos.cs index a5cb95e9..7dfcba58 100644 --- a/src/UiPath.CoreIpc/Wire/Dtos.cs +++ b/src/UiPath.CoreIpc/Wire/Dtos.cs @@ -38,14 +38,14 @@ internal record Response(string RequestId, string? Data = null, Error? Error = n 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) + public TResult Deserialize() { if (Error != null) { throw new RemoteException(Error); } - return (TResult)(DownloadStream ?? serializer.OrDefault().Deserialize(Data ?? "", typeof(TResult)))!; + return (TResult)(DownloadStream ?? IpcJsonSerializer.Instance.Deserialize(Data ?? "", typeof(TResult)))!; } } [Serializable] diff --git a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs index a9e6d712..feb7a455 100644 --- a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs +++ b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs @@ -5,15 +5,7 @@ namespace UiPath.Ipc; -internal interface ISerializer -{ - ValueTask DeserializeAsync(Stream json, ILogger? logger); - void Serialize(object? obj, Stream stream); - string Serialize(object? obj); - object? Deserialize(string json, Type type); -} - -internal class IpcJsonSerializer : ISerializer, IArrayPool +internal class IpcJsonSerializer : IArrayPool { public static readonly IpcJsonSerializer Instance = new(); diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.Ipc.Tests/ComputingTests.cs index 24fdacb1..e6858e6b 100644 --- a/src/UiPath.Ipc.Tests/ComputingTests.cs +++ b/src/UiPath.Ipc.Tests/ComputingTests.cs @@ -1,5 +1,4 @@ -using AutoFixture; -using Newtonsoft.Json; +using Newtonsoft.Json; using Nito.AsyncEx; using Nito.Disposables; using NSubstitute; @@ -9,7 +8,6 @@ using UiPath.Ipc.Transport.NamedPipe; using UiPath.Ipc.Transport.Tcp; using UiPath.Ipc.Transport.WebSocket; -using Xunit; using Xunit.Abstractions; namespace UiPath.Ipc.Tests; @@ -290,18 +288,18 @@ public readonly record struct ExternalServerParams(ServerKind Kind, string? Pipe { case ServerKind.NamedPipes: { - listenerConfig = new NamedPipeListener() { PipeName = PipeName! }; + listenerConfig = new NamedPipeServerTransport() { PipeName = PipeName! }; return null; } case ServerKind.Tcp: { - listenerConfig = new TcpListener() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }; + listenerConfig = new TcpServerTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }; return null; } case ServerKind.WebSockets: { var context = new WebSocketContext(Port); - listenerConfig = new WebSocketListener { Accept = context.Accept }; + listenerConfig = new WebSocketServerTransport { Accept = context.Accept }; return context; } default: @@ -311,7 +309,7 @@ public readonly record struct ExternalServerParams(ServerKind Kind, string? Pipe public ClientTransport CreateClientTransport() => Kind switch { - ServerKind.NamedPipes => new NamedPipeTransport() { PipeName = PipeName! }, + ServerKind.NamedPipes => new NamedPipeClientTransport() { PipeName = PipeName! }, ServerKind.Tcp => new TcpTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }, ServerKind.WebSockets => new WebSocketTransport() { Uri = new($"ws://localhost:{Port}") }, _ => throw new NotSupportedException($"Kind not supported. Kind was {Kind}") diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs index 0a6c67af..067b5ba7 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs @@ -9,11 +9,11 @@ public sealed class ComputingTestsOverNamedPipes : ComputingTests public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeListener + protected override async Task CreateListener() => new NamedPipeServerTransport { PipeName = PipeName }; - protected override ClientTransport CreateClientTransport() => new NamedPipeTransport + protected override ClientTransport CreateClientTransport() => new NamedPipeClientTransport { PipeName = PipeName, AllowImpersonation = true, @@ -22,8 +22,8 @@ public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outpu public override IAsyncDisposable? RandomTransportPair(out ServerTransport listener, out ClientTransport transport) { var pipeName = $"{Guid.NewGuid():N}"; - listener = new NamedPipeListener { PipeName = pipeName }; - transport = new NamedPipeTransport { PipeName = pipeName }; + listener = new NamedPipeServerTransport { PipeName = pipeName }; + transport = new NamedPipeClientTransport { PipeName = pipeName }; return null; } diff --git a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs index 665dbab2..71fa5d83 100644 --- a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs +++ b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs @@ -27,7 +27,7 @@ private static IpcServer CreateServer(string pipeName) => new IpcServer { Transport = [ - new NamedPipeListener + new NamedPipeServerTransport { PipeName = pipeName, } @@ -45,7 +45,7 @@ private static IpcServer CreateServer(string pipeName) private static IpcClient CreateClient(string pipeName) => new() { - Transport = new NamedPipeTransport { PipeName = pipeName }, + Transport = new NamedPipeClientTransport { PipeName = pipeName }, Config = new() }; diff --git a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs index 22ef42cb..fed8d637 100644 --- a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs @@ -12,11 +12,11 @@ public sealed class RobotTestsOverNamedPipes : RobotTests public RobotTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeListener + protected override async Task CreateListener() => new NamedPipeServerTransport { PipeName = PipeName }; - protected override ClientTransport CreateClientTransport() => new NamedPipeTransport + protected override ClientTransport CreateClientTransport() => new NamedPipeClientTransport { PipeName = PipeName, AllowImpersonation = true, @@ -62,7 +62,7 @@ public static void OnConnectingToUserService() public TContract CreateUserServiceProxy(string pipeName) where TContract : class => RobotIpcHelpers.CreateProxy( - listener: new NamedPipeListener() + listener: new NamedPipeServerTransport() { PipeName = pipeName, AccessControl = security => security.AllowCurrentUser(), @@ -82,7 +82,7 @@ internal static partial class RobotIpcHelpers private static readonly ConcurrentDictionary PipeClients = new(); public static TContract CreateProxy( - NamedPipeListener listener, + NamedPipeServerTransport listener, EndpointCollection? callbacks = null, IServiceProvider? provider = null, Action? beforeConnect = null, @@ -153,7 +153,7 @@ private static ClientAndParams CreateClient(CreateProxyRequest request) BeforeCall = request.Params.BeforeCall, Scheduler = request.Params.Scheduler, }, - Transport = new NamedPipeTransport + Transport = new NamedPipeClientTransport { PipeName = request.ActualKey.Name, AllowImpersonation = request.ActualKey.AllowImpersonation, diff --git a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs index afc79a1d..01d4ef79 100644 --- a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs +++ b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs @@ -48,7 +48,7 @@ private static IpcServer CreateServer(string pipeName) => new IpcServer { Transport = [ - new NamedPipeListener + new NamedPipeServerTransport { PipeName = pipeName, } @@ -66,7 +66,7 @@ private static IpcServer CreateServer(string pipeName) private static IpcClient CreateClient(string pipeName) => new() { - Transport = new NamedPipeTransport { PipeName = pipeName }, + Transport = new NamedPipeClientTransport { PipeName = pipeName }, Config = new() }; diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs index 5d173821..d0430161 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs @@ -9,11 +9,11 @@ public sealed class SystemTestsOverNamedPipes : SystemTests public SystemTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected sealed override async Task CreateListener() => new NamedPipeListener + protected sealed override async Task CreateListener() => new NamedPipeServerTransport { PipeName = PipeName }; - protected sealed override ClientTransport CreateClientTransport() => new NamedPipeTransport() + protected sealed override ClientTransport CreateClientTransport() => new NamedPipeClientTransport() { PipeName = PipeName, AllowImpersonation = true, diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs index 97432053..265a9134 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs @@ -11,7 +11,7 @@ public sealed class SystemTestsOverTcp : SystemTests public SystemTestsOverTcp(ITestOutputHelper outputHelper) : base(outputHelper) { } protected sealed override async Task CreateListener() - => new TcpListener + => new TcpServerTransport { EndPoint = _endPoint, }; diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs index 235f0518..b8795dd2 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs @@ -17,7 +17,7 @@ protected override async Task DisposeAsync() protected override async Task CreateListener() { - var listener = new WebSocketListener + var listener = new WebSocketServerTransport { Accept = _webSocketContext.Accept, ConcurrentAccepts = 1, From 6b724fc8cc9941c44cb9399b8678d9f2d2b41317 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 25 Nov 2024 13:28:04 +0100 Subject: [PATCH 04/26] taking shape --- src/CoreIpc.sln | 8 +- src/Playground/Program.cs | 43 ++- ...ath.CoreIpc.Extensions.Abstractions.csproj | 17 ++ src/UiPath.CoreIpc.Http/BidiHttpListener.cs | 285 ------------------ .../BidiHttpServerTransport.cs | 278 +++++++++++++++++ .../UiPath.CoreIpc.Http.csproj | 2 +- src/UiPath.CoreIpc/Client/ServiceClient.cs | 32 +- src/UiPath.CoreIpc/Config/ClientConfig.cs | 43 +-- ...erviceClientConfig.cs => IClientConfig.cs} | 6 +- src/UiPath.CoreIpc/Config/IClientState.cs | 9 + src/UiPath.CoreIpc/Config/IpcClient.cs | 50 ++- src/UiPath.CoreIpc/Config/IpcServer.cs | 26 +- src/UiPath.CoreIpc/Config/Peer.cs | 10 +- src/UiPath.CoreIpc/Config/ServerTransport.cs | 5 +- src/UiPath.CoreIpc/Helpers/Router.cs | 2 +- src/UiPath.CoreIpc/Server/EndpointSettings.cs | 2 +- src/UiPath.CoreIpc/Server/ServerConnection.cs | 12 +- ...{TcpTransport.cs => TcpClientTransport.cs} | 4 +- ...ansport.cs => WebSocketClientTransport.cs} | 4 +- src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 8 + src/UiPath.Ipc.Tests/ComputingTests.cs | 51 +--- .../ComputingTestsOverNamedPipes.cs | 2 +- .../Config/OverrideConfigAttribute.cs | 4 +- src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs | 28 +- src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs | 13 +- src/UiPath.Ipc.Tests/Program.cs | 4 +- src/UiPath.Ipc.Tests/RobotTests.cs | 23 +- .../RobotTestsOverNamedPipes.cs | 43 +-- src/UiPath.Ipc.Tests/SpyTestBase.cs | 20 ++ src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs | 10 +- src/UiPath.Ipc.Tests/SystemTests.cs | 38 +-- .../SystemTestsOverNamedPipes.cs | 2 +- src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs | 4 +- .../SystemTestsOverWebSockets.cs | 4 +- src/UiPath.Ipc.Tests/TestBase.cs | 104 ++++--- 35 files changed, 594 insertions(+), 602 deletions(-) create mode 100644 src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj delete mode 100644 src/UiPath.CoreIpc.Http/BidiHttpListener.cs create mode 100644 src/UiPath.CoreIpc.Http/BidiHttpServerTransport.cs rename src/UiPath.CoreIpc/Config/{IServiceClientConfig.cs => IClientConfig.cs} (58%) create mode 100644 src/UiPath.CoreIpc/Config/IClientState.cs rename src/UiPath.CoreIpc/Transport/Tcp/{TcpTransport.cs => TcpClientTransport.cs} (89%) rename src/UiPath.CoreIpc/Transport/WebSocket/{WebSocketTransport.cs => WebSocketClientTransport.cs} (84%) create mode 100644 src/UiPath.Ipc.Tests/SpyTestBase.cs diff --git a/src/CoreIpc.sln b/src/CoreIpc.sln index 12d4a07d..74779ed7 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# 17 +# Visual Studio Version 17 VisualStudioVersion = 17.0.31919.166 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc", "UiPath.CoreIpc\UiPath.CoreIpc.csproj", "{58200319-1F71-4E22-894D-7E69E0CD0B57}" @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc.Http", "UiPa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.Ipc.Tests", "UiPath.Ipc.Tests\UiPath.Ipc.Tests.csproj", "{E238E183-92CF-48A6-890F-C422853D6656}" 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.Build.0 = Debug|Any CPU {E238E183-92CF-48A6-890F-C422853D6656}.Release|Any CPU.ActiveCfg = Release|Any CPU {E238E183-92CF-48A6-890F-C422853D6656}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index 613e8d5a..e373bd18 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -44,7 +44,7 @@ private static async Task Main(string[] args) typeof(Contracts.IServerOperations), // DEVINE new EndpointSettings(typeof(Contracts.IServerOperations)) // ASTALALT { - BeforeCall = async (callInfo, _) => + BeforeIncommingCall = async (callInfo, _) => { Console.WriteLine($"Server: {callInfo.Method.Name}"); } @@ -76,36 +76,30 @@ private static async Task Main(string[] args) var c1 = new IpcClient() { - Config = new() + Callbacks = new() { - Callbacks = new() - { - typeof(Contracts.IClientOperations), - { typeof(Contracts.IClientOperations2), new Impl.Client2() }, - }, - ServiceProvider = clientSP, - Scheduler = clientScheduler, + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, }, + ServiceProvider = clientSP, + Scheduler = clientScheduler, Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, ServerName = ".", AllowImpersonation = false, - }, + } }; var c2 = new IpcClient() { - Config = new() + ServiceProvider = clientSP, + Callbacks = new() { - ServiceProvider = clientSP, - Callbacks = new() - { - typeof(Contracts.IClientOperations), - { typeof(Contracts.IClientOperations2), new Impl.Client2() }, - }, - Scheduler = clientScheduler, + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, }, + Scheduler = clientScheduler, Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, @@ -116,16 +110,13 @@ private static async Task Main(string[] args) var proxy1 = new IpcClient() { - Config = new() + ServiceProvider = clientSP, + Callbacks = new() { - ServiceProvider = clientSP, - Callbacks = new() - { - typeof(Contracts.IClientOperations), - { typeof(Contracts.IClientOperations2), new Impl.Client2() }, - }, - Scheduler = clientScheduler, + typeof(Contracts.IClientOperations), + { typeof(Contracts.IClientOperations2), new Impl.Client2() }, }, + Scheduler = clientScheduler, Transport = new NamedPipeClientTransport() { PipeName = Contracts.PipeName, 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..2186c679 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj @@ -0,0 +1,17 @@ + + + + net6.0;net461;net6.0-windows + enable + enable + preview + true + enable + true + + + + + + + diff --git a/src/UiPath.CoreIpc.Http/BidiHttpListener.cs b/src/UiPath.CoreIpc.Http/BidiHttpListener.cs deleted file mode 100644 index 06076a21..00000000 --- a/src/UiPath.CoreIpc.Http/BidiHttpListener.cs +++ /dev/null @@ -1,285 +0,0 @@ -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.Http; - -using static Constants; -using IBidiHttpListenerConfig = IListenerConfig; - -public sealed partial record BidiHttpListener : ServerTransport, IBidiHttpListenerConfig -{ - public required Uri Uri { get; init; } - - BidiHttpListenerState IBidiHttpListenerConfig.CreateListenerState(IpcServer server) - => new(server, this); - - BidiHttpServerConnectionState IBidiHttpListenerConfig.CreateConnectionState(IpcServer server, BidiHttpListenerState listenerState) - => new(server, listenerState); - - async ValueTask IBidiHttpListenerConfig.AwaitConnection(BidiHttpListenerState listenerState, BidiHttpServerConnectionState connectionState, CancellationToken ct) - { - await connectionState.WaitForConnection(ct); - return connectionState; - } - - public IEnumerable Validate() - { - throw new NotImplementedException(); - } -} - -internal sealed class BidiHttpListenerState : IAsyncDisposable -{ - private readonly IpcServer _ipcServer; - 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 BidiHttpListenerState(IpcServer ipcServer, BidiHttpListener listener) - { - _ipcServer = ipcServer; - _httpListener = new HttpListener() - { - Prefixes = - { - listener.Uri.ToString() - } - }; - _processing = ProcessContexts(); - _disposing = new(DisposeCore); - } - - public 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; - } - } -} - -internal sealed class BidiHttpServerConnectionState : Stream, IAsyncDisposable -{ - private readonly Pipe _pipe = new(); - - private readonly IpcServer _server; - private readonly BidiHttpListenerState _listenerState; - - private readonly CancellationTokenSource _cts = new(); - private readonly AsyncLock _lock = new(); - private (Guid connectionId, Uri reverseUri)? _connection = null; - private HttpClient? _client; - private Task? _processing = null; - private readonly Lazy _disposing; - - public BidiHttpServerConnectionState(IpcServer server, BidiHttpListenerState listenerState) - { - _server = server; - _listenerState = listenerState; - _disposing = new(DisposeCore); - } - - public -#if !NET461 - override -#endif - ValueTask DisposeAsync() => new(_disposing.Value); - - 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 async Task WaitForConnection(CancellationToken ct) - { - using (await _lock.LockAsync(ct)) - { - if (_connection is not null) - { - throw new InvalidOperationException(); - } - - _connection = await _listenerState.NewConnections.ReadAsync(ct); - - _client = new() - { - BaseAddress = _connection.Value.reverseUri, - DefaultRequestHeaders = - { - { ConnectionIdHeader, _connection.Value.connectionId.ToString() } - } - }; - - _processing = ProcessContexts(_cts.Token); - } - } - - private async Task ProcessContexts(CancellationToken ct) - { - var reader = _listenerState.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(); - } - } - } - - 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 _pipe.Reader.ReadAsync(ct); - - var take = (int)Math.Min(readResult.Buffer.Length, memory.Length); - - readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); - _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 (_client is null) - { - throw new InvalidOperationException(); - } - - HttpContent content = -#if NET461 - new ByteArrayContent(memory.ToArray()); -#else - new ReadOnlyMemoryContent(memory); -#endif - - await _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.Http/BidiHttpServerTransport.cs b/src/UiPath.CoreIpc.Http/BidiHttpServerTransport.cs new file mode 100644 index 00000000..4e43ce58 --- /dev/null +++ b/src/UiPath.CoreIpc.Http/BidiHttpServerTransport.cs @@ -0,0 +1,278 @@ +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.Http; + +using static Constants; + +public sealed partial class BidiHttpServerTransport : ServerTransport +{ + public required Uri Uri { get; init; } + + protected override IServerState CreateServerState() + => new BidiHttpServerState(this); + + protected override IEnumerable ValidateCore() => []; + + private sealed class BidiHttpServerState : IServerState + { + 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 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; + } + } + + IServerConnectionSlot IServerState.CreateConnectionSlot() => new BidiHttpServerConnectionSlot(this); + } + + private sealed class BidiHttpServerConnectionSlot : Stream, IServerConnectionSlot, IAsyncDisposable + { + private readonly Pipe _pipe = new(); + + private readonly BidiHttpServerState _listenerState; + + private readonly CancellationTokenSource _cts = new(); + private readonly AsyncLock _lock = new(); + private (Guid connectionId, Uri reverseUri)? _connection = null; + private HttpClient? _client; + private Task? _processing = null; + private readonly Lazy _disposing; + + public BidiHttpServerConnectionSlot(BidiHttpServerState serverState) + { + _listenerState = serverState; + _disposing = new(DisposeCore); + } + + public +#if !NET461 + override +#endif + ValueTask DisposeAsync() => new(_disposing.Value); + + 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 async Task WaitForConnection(CancellationToken ct) + { + using (await _lock.LockAsync(ct)) + { + if (_connection is not null) + { + throw new InvalidOperationException(); + } + + _connection = await _listenerState.NewConnections.ReadAsync(ct); + + _client = new() + { + BaseAddress = _connection.Value.reverseUri, + DefaultRequestHeaders = + { + { ConnectionIdHeader, _connection.Value.connectionId.ToString() } + } + }; + + _processing = ProcessContexts(_cts.Token); + } + } + + private async Task ProcessContexts(CancellationToken ct) + { + var reader = _listenerState.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(); + } + } + } + + ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + { + throw new NotImplementedException(); + } + + + + 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 _pipe.Reader.ReadAsync(ct); + + var take = (int)Math.Min(readResult.Buffer.Length, memory.Length); + + readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); + _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 (_client is null) + { + throw new InvalidOperationException(); + } + + HttpContent content = +#if NET461 + new ByteArrayContent(memory.ToArray()); +#else + new ReadOnlyMemoryContent(memory); +#endif + + await _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.Http/UiPath.CoreIpc.Http.csproj b/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj index 524ffbc1..83f0f21e 100644 --- a/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj +++ b/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/UiPath.CoreIpc/Client/ServiceClient.cs b/src/UiPath.CoreIpc/Client/ServiceClient.cs index 4c0edd2f..95ef34c3 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -11,7 +11,7 @@ private static IpcProxy CreateProxy(ServiceClient serviceClient) where T : cl return proxy; } - protected abstract IServiceClientConfig Config { get; } + protected abstract IClientConfig Config { get; } public abstract Stream? Network { get; } public event EventHandler? ConnectionClosed; @@ -66,11 +66,7 @@ async Task Invoke() var (connection, newConnection) = await EnsureConnection(ct); - if (Config.BeforeCall is not null) - { - var callInfo = new CallInfo(newConnection, method, args); - await Config.BeforeCall(callInfo, 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) @@ -78,18 +74,18 @@ async Task Invoke() UploadStream = uploadStream }; - Config.Logger?.ServiceClient_Calling(methodName, requestId, Config.DebugName); + Config.Logger?.ServiceClient_Calling(methodName, requestId, Config.GetComputedDebugName()); Response response; try { 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.DebugName); + Config.Logger?.ServiceClient_CalledSuccessfully(request.MethodName, requestId, Config.GetComputedDebugName()); } catch (Exception ex) { - Config.Logger?.ServiceClient_FailedToCall(request.MethodName, requestId, Config.DebugName, ex); + Config.Logger?.ServiceClient_FailedToCall(request.MethodName, requestId, Config.GetComputedDebugName(), ex); throw; } @@ -133,7 +129,7 @@ string[] SerializeArguments() public abstract void Dispose(); - public override string ToString() => Config.DebugName; + public override string ToString() => Config.GetComputedDebugName(); #region Generic adapter cache private static readonly MethodInfo GenericDefOf_Invoke = ((Func>)Invoke).Method.GetGenericMethodDefinition(); @@ -227,9 +223,9 @@ public override async ValueTask CloseConnection() var network = await Connect(ct); - LatestConnection = new Connection(network, Config.DebugName, Config.Logger); - var router = new Router(_client.Config.CreateCallbackRouterConfig(), _client.Config.ServiceProvider); - _latestServer = new Server(router, _client.Config.RequestTimeout, LatestConnection); + 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); @@ -242,7 +238,7 @@ async Task Pal() } catch (Exception ex) { - Config.Logger.LogException(ex, Config.DebugName); + Config.Logger.LogException(ex, Config.GetComputedDebugName()); } } } @@ -260,17 +256,17 @@ private async Task Connect(CancellationToken ct) return network; } - protected override IServiceClientConfig Config => _client.Config; + protected override IClientConfig Config => _client; } internal sealed class ServiceClientForCallback : ServiceClient where TInterface : class { private readonly Connection _connection; - private readonly IServiceClientConfig _config; + private readonly IClientConfig _config; public override Stream? Network => _connection.Network; - public ServiceClientForCallback(Connection connection, IServiceClientConfig config) : base(typeof(TInterface)) + public ServiceClientForCallback(Connection connection, IClientConfig config) : base(typeof(TInterface)) { _connection = connection; _config = config; @@ -286,5 +282,5 @@ public override void Dispose() protected override Task<(Connection connection, bool newlyConnected)> EnsureConnection(CancellationToken ct) => Task.FromResult((_connection, newlyConnected: false)); - protected override IServiceClientConfig Config => _config; + protected override IClientConfig Config => _config; } diff --git a/src/UiPath.CoreIpc/Config/ClientConfig.cs b/src/UiPath.CoreIpc/Config/ClientConfig.cs index c6ac4bdb..6c05c192 100644 --- a/src/UiPath.CoreIpc/Config/ClientConfig.cs +++ b/src/UiPath.CoreIpc/Config/ClientConfig.cs @@ -1,18 +1,9 @@ -using System.ComponentModel; - -namespace UiPath.Ipc; +namespace UiPath.Ipc; public sealed class ClientConfig : Peer, IServiceClientConfig { - public EndpointCollection? Callbacks { get; init; } - - public ILogger? Logger { get; init; } - public BeforeConnectHandler? BeforeConnect { get; init; } public BeforeCallHandler? BeforeCall { get; init; } - [EditorBrowsable(EditorBrowsableState.Never)] - public string DebugName { get; set; } = null!; - internal void Validate() { var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; @@ -22,36 +13,4 @@ internal void Validate() throw new InvalidOperationException("ServiceProvider is required when you register injectable callbacks. Consider registering a callback instance."); } } - - internal ILogger? GetLogger(string name) - { - if (Logger is not null) - { - return Logger; - } - - if (ServiceProvider?.GetService() is not { } loggerFactory) - { - return null; - } - - return loggerFactory.CreateLogger(name); - } - - internal override RouterConfig CreateCallbackRouterConfig() - => RouterConfig.From( - Callbacks.OrDefault(), - endpoint => endpoint with - { - BeforeCall = null, // callbacks don't support BeforeCall - Scheduler = endpoint.Scheduler ?? Scheduler - }); } - -public interface IClientState : IDisposable -{ - Stream? Network { get; } - - bool IsConnected(); - ValueTask Connect(IpcClient client, CancellationToken ct); -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs b/src/UiPath.CoreIpc/Config/IClientConfig.cs similarity index 58% rename from src/UiPath.CoreIpc/Config/IServiceClientConfig.cs rename to src/UiPath.CoreIpc/Config/IClientConfig.cs index 19ed49fd..9e4741ab 100644 --- a/src/UiPath.CoreIpc/Config/IServiceClientConfig.cs +++ b/src/UiPath.CoreIpc/Config/IClientConfig.cs @@ -1,11 +1,11 @@ namespace UiPath.Ipc; // Maybe decommission -internal interface IServiceClientConfig +internal interface IClientConfig { TimeSpan RequestTimeout { get; } BeforeConnectHandler? BeforeConnect { get; } - BeforeCallHandler? BeforeCall { get; } + BeforeCallHandler? BeforeOutgoingCall { get; } ILogger? Logger { get; } - string DebugName { 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..74ff640c --- /dev/null +++ b/src/UiPath.CoreIpc/Config/IClientState.cs @@ -0,0 +1,9 @@ +namespace UiPath.Ipc; + +public interface IClientState : IDisposable +{ + Stream? Network { get; } + + bool IsConnected(); + ValueTask Connect(IpcClient client, CancellationToken ct); +} diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index 24104b16..7bed1c05 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -1,10 +1,22 @@ -namespace UiPath.Ipc; +using System.ComponentModel; -public sealed class IpcClient : Peer +namespace UiPath.Ipc; + +public sealed class IpcClient : Peer, IClientConfig { - public required ClientConfig Config { get; init; } + public EndpointCollection? Callbacks { get; set; } + + public ILogger? Logger { get; init; } + public BeforeConnectHandler? BeforeConnect { get; set; } + public BeforeCallHandler? BeforeOutgoingCall { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public 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) { @@ -16,18 +28,42 @@ private ServiceClient GetServiceClient(Type proxyType) internal void Validate() { - if (Config is null) + var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; + + if (haveDeferredInjectedCallbacks && ServiceProvider is null) { - throw new InvalidOperationException($"{Config} is required."); + throw new InvalidOperationException("ServiceProvider is required when you register injectable callbacks. Consider registering a callback instance."); } + if (Transport is null) { throw new InvalidOperationException($"{Transport} is required."); } - Config.Validate(); Transport.Validate(); + } - Config.DebugName ??= Transport.ToString(); + internal ILogger? GetLogger(string name) + { + if (Logger is not null) + { + return Logger; + } + + if (ServiceProvider?.GetService() is not { } loggerFactory) + { + return null; + } + + return loggerFactory.CreateLogger(name); } + + internal RouterConfig CreateCallbackRouterConfig() + => RouterConfig.From( + Callbacks.OrDefault(), + endpoint => endpoint with + { + BeforeIncommingCall = null, // callbacks don't support BeforeCall + Scheduler = endpoint.Scheduler ?? Scheduler + }); } diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index d0f58020..e9c3d14d 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -14,8 +14,16 @@ public sealed class IpcServer : Peer, IAsyncDisposable private bool _disposeStarted; private Accepter? _accepter; + private Lazy _dispose; - public async ValueTask DisposeAsync() + public IpcServer() + { + _dispose = new(DisposeCore); + } + + public ValueTask DisposeAsync() => new(_dispose.Value); + + private async Task DisposeCore() { Accepter? accepter = null; lock (_lock) @@ -77,6 +85,13 @@ private void OnNewConnectionError(Exception ex) _stopped.TrySetException(ex); } + internal RouterConfig CreateRouterConfig(IpcServer server) => RouterConfig.From( + server.Endpoints, + endpoint => endpoint with + { + Scheduler = endpoint.Scheduler ?? server.Scheduler + }); + private sealed class ObserverAdapter : IObserver { public required Action OnNext { get; init; } @@ -95,6 +110,7 @@ private sealed class Accepter : IAsyncDisposable private readonly Task _running; private readonly IObserver _newConnection; private readonly TaskCompletionSource _tcsStartedAccepting = new(); + private readonly Lazy _dispose; public Task StartedAccepting => _tcsStartedAccepting.Task; @@ -103,15 +119,19 @@ public Accepter(ServerTransport transport, IObserver connected) _serverState = transport.CreateServerState(); _newConnection = connected; _running = RunOnThreadPool(LoopAccept, parallelCount: transport.ConcurrentAccepts, _cts.Token); + _dispose = new(DisposeCore); } - public async ValueTask DisposeAsync() - { + public ValueTask DisposeAsync() => new(_dispose.Value); + + private async Task DisposeCore() + { _cts.Cancel(); await _running; _cts.Dispose(); } + private async Task LoopAccept(CancellationToken ct) { try diff --git a/src/UiPath.CoreIpc/Config/Peer.cs b/src/UiPath.CoreIpc/Config/Peer.cs index c7e9ba8a..4d4f7121 100644 --- a/src/UiPath.CoreIpc/Config/Peer.cs +++ b/src/UiPath.CoreIpc/Config/Peer.cs @@ -2,11 +2,7 @@ public abstract class Peer { - public TimeSpan RequestTimeout { get; init; } = Timeout.InfiniteTimeSpan; - public IServiceProvider? ServiceProvider { get; init; } - public TaskScheduler? Scheduler { get; init; } - - internal virtual RouterConfig CreateRouterConfig(IpcServer server) => throw new NotSupportedException(); - - internal virtual RouterConfig CreateCallbackRouterConfig() => throw new NotSupportedException(); + public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; + public IServiceProvider? ServiceProvider { get; set; } + public TaskScheduler? Scheduler { get; set; } } diff --git a/src/UiPath.CoreIpc/Config/ServerTransport.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs index ed49685e..c63bc155 100644 --- a/src/UiPath.CoreIpc/Config/ServerTransport.cs +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -5,8 +5,9 @@ namespace UiPath.Ipc; public abstract class ServerTransport { - public int ConcurrentAccepts { get; init; } = 5; - public byte MaxReceivedMessageSizeInMegabytes { get; init; } = 2; + public int ConcurrentAccepts { get; set; } = 5; + public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; + public X509Certificate? Certificate { get; init; } internal int MaxMessageSize => MaxReceivedMessageSizeInMegabytes * 1024 * 1024; diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs index 45252243..26684819 100644 --- a/src/UiPath.CoreIpc/Helpers/Router.cs +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -131,7 +131,7 @@ public static Route From(IServiceProvider? serviceProvider, EndpointSettings end => new Route() { Service = endpointSettings.Service.WithProvider(serviceProvider), - BeforeCall = endpointSettings.BeforeCall, + BeforeCall = endpointSettings.BeforeIncommingCall, Scheduler = endpointSettings.Scheduler.OrDefault(), LoggerFactory = serviceProvider.MaybeCreateServiceFactory(), }; diff --git a/src/UiPath.CoreIpc/Server/EndpointSettings.cs b/src/UiPath.CoreIpc/Server/EndpointSettings.cs index 26a39d6b..3b00df73 100644 --- a/src/UiPath.CoreIpc/Server/EndpointSettings.cs +++ b/src/UiPath.CoreIpc/Server/EndpointSettings.cs @@ -5,7 +5,7 @@ public record EndpointSettings { public TaskScheduler? Scheduler { get; set; } - public BeforeCallHandler? BeforeCall { get; set; } + public BeforeCallHandler? BeforeIncommingCall { get; set; } public Type ContractType => Service.Type; public object? ServiceInstance => Service.MaybeGetInstance(); public IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider(); diff --git a/src/UiPath.CoreIpc/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index cde20546..276985b1 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -internal sealed class ServerConnection : IClient, IDisposable, IServiceClientConfig +internal sealed class ServerConnection : IClient, IDisposable, IClientConfig { public static void CreateAndListen(IpcServer server, Stream network, CancellationToken ct) { @@ -71,10 +71,10 @@ void IClient.Impersonate(Action action) } #region IServiceClientConfig - TimeSpan IServiceClientConfig.RequestTimeout => _ipcServer.RequestTimeout; - BeforeConnectHandler? IServiceClientConfig.BeforeConnect => null; - BeforeCallHandler? IServiceClientConfig.BeforeCall => null; - ILogger? IServiceClientConfig.Logger => _logger; - string IServiceClientConfig.DebugName => _debugName; + 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/Transport/Tcp/TcpTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs similarity index 89% rename from src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs rename to src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs index c114f4f6..dd7cf5a3 100644 --- a/src/UiPath.CoreIpc/Transport/Tcp/TcpTransport.cs +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc.Transport.Tcp; -public sealed record TcpTransport : ClientTransport +public sealed record TcpClientTransport : ClientTransport { public required IPEndPoint EndPoint { get; init; } @@ -32,7 +32,7 @@ public bool IsConnected() public async ValueTask Connect(IpcClient client, CancellationToken ct) { - var transport = client.Transport as TcpTransport ?? throw new InvalidOperationException(); + var transport = client.Transport as TcpClientTransport ?? throw new InvalidOperationException(); _tcpClient = new System.Net.Sockets.TcpClient(); #if NET461 diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs similarity index 84% rename from src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs rename to src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs index 6558b2c7..e5b36472 100644 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketTransport.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc.Transport.WebSocket; -public sealed record WebSocketTransport : ClientTransport +public sealed record WebSocketClientTransport : ClientTransport { public required Uri Uri { get; init; } public override string ToString() => $"WebSocketClient={Uri}"; @@ -28,7 +28,7 @@ internal sealed class WebSocketClientState : IClientState public async ValueTask Connect(IpcClient client, CancellationToken ct) { - var transport = client.Transport as WebSocketTransport ?? throw new InvalidOperationException(); + var transport = client.Transport as WebSocketClientTransport ?? throw new InvalidOperationException(); _clientWebSocket = new(); await _clientWebSocket.ConnectAsync(transport.Uri, ct); diff --git a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index af37d5f6..d2bb74a6 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -27,6 +27,13 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -35,6 +42,7 @@ + diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.Ipc.Tests/ComputingTests.cs index e6858e6b..aeecd313 100644 --- a/src/UiPath.Ipc.Tests/ComputingTests.cs +++ b/src/UiPath.Ipc.Tests/ComputingTests.cs @@ -2,7 +2,6 @@ using Nito.AsyncEx; using Nito.Disposables; using NSubstitute; -using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text; using UiPath.Ipc.Transport.NamedPipe; @@ -12,7 +11,7 @@ namespace UiPath.Ipc.Tests; -public abstract class ComputingTests : TestBase +public abstract class ComputingTests : SpyTestBase { #region " Setup " protected readonly ComputingCallback _computingCallback = new(); @@ -25,8 +24,10 @@ public abstract class ComputingTests : TestBase protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; protected sealed override Type ContractType => typeof(IComputingService); - - protected readonly ConcurrentBag _clientBeforeCalls = new(); + protected override EndpointCollection? Callbacks => new() + { + { typeof(IComputingCallback), _computingCallback } + }; protected ComputingTests(ITestOutputHelper outputHelper) : base(outputHelper) { @@ -42,24 +43,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingletonAlias() ; - protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) - => listener with - { - ConcurrentAccepts = 10, - RequestTimeout = Timeouts.DefaultRequest, - MaxReceivedMessageSizeInMegabytes = 1, - }; - protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) - => new() - { - RequestTimeout = Timeouts.DefaultRequest, - Scheduler = GuiScheduler, - Callbacks = callbacks ?? new() - { - { typeof(IComputingCallback), _computingCallback } - }, - BeforeCall = async (callInfo, _) => _clientBeforeCalls.Add(callInfo), - }; + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; #endregion [Theory, IpcAutoData] @@ -231,16 +215,13 @@ public async Task BeforeConnect_ShouldStartExternalServerJIT() }); var proxy = new IpcClient { - Config = new() + Scheduler = GuiScheduler, + BeforeConnect = async (_) => { - Scheduler = GuiScheduler, - BeforeConnect = async (_) => - { - serverProcess.Start(); - var time = TimeSpan.FromSeconds(1); - _outputHelper.WriteLine($"Server started. Waiting {time}. PID={serverProcess.Id}"); - await Task.Delay(time); - }, + serverProcess.Start(); + var time = TimeSpan.FromSeconds(1); + _outputHelper.WriteLine($"Server started. Waiting {time}. PID={serverProcess.Id}"); + await Task.Delay(time); }, Transport = externalServerParams.CreateClientTransport() }.GetProxy(); @@ -260,7 +241,7 @@ await Enumerable.Range(1, CParallelism) var mockCallback = Substitute.For(); mockCallback.AddInts(0, 1).Returns(1); - var proxy = CreateClient(callbacks: new() + var proxy = CreateIpcClient(callbacks: new() { { typeof(IComputingCallback), mockCallback } })!.GetProxy(); @@ -310,8 +291,8 @@ public readonly record struct ExternalServerParams(ServerKind Kind, string? Pipe public ClientTransport CreateClientTransport() => Kind switch { ServerKind.NamedPipes => new NamedPipeClientTransport() { PipeName = PipeName! }, - ServerKind.Tcp => new TcpTransport() { EndPoint = new(System.Net.IPAddress.Loopback, Port) }, - ServerKind.WebSockets => new WebSocketTransport() { Uri = new($"ws://localhost:{Port}") }, + 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}") }; } @@ -319,7 +300,7 @@ public enum ServerKind { NamedPipes, Tcp, WebSockets } private sealed class DisableInProcClientServer : OverrideConfig { - public override async Task Override(Func> listener) => null; + public override async Task Override(Func> ipcServerFactory) => null; public override IpcClient? Override(Func client) => null; } } diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs index 067b5ba7..88ef854f 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs @@ -9,7 +9,7 @@ public sealed class ComputingTestsOverNamedPipes : ComputingTests public ComputingTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeServerTransport + protected override async Task CreateServerTransport() => new NamedPipeServerTransport { PipeName = PipeName }; diff --git a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs index 237c1e40..cecdedde 100644 --- a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs +++ b/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs @@ -34,6 +34,6 @@ public OverrideConfigAttribute(Type overrideConfigType) public abstract class OverrideConfig { - public virtual Task Override(Func> listener) => listener()!; - public virtual IpcClient? Override(Func client) => client(); + 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.Ipc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs index 0296354b..3ccc33c9 100644 --- a/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs +++ b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs @@ -35,23 +35,27 @@ public static IServiceProvider GetRequired(this IServiceProvider serviceProvi internal static class IpcClientExtensions { public static IpcClient WithRequestTimeout(this IpcClient ipcClient, TimeSpan requestTimeout) - => new() { - Config = ipcClient.Config with { RequestTimeout = requestTimeout }, - Transport = ipcClient.Transport, - }; + 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, EndpointCollection callbacks) - => new() { - Config = ipcClient.Config with { Callbacks = callbacks }, - Transport = ipcClient.Transport, - }; + ipcClient.Callbacks = callbacks; + return ipcClient; + } public static IpcClient WithBeforeConnect(this IpcClient ipcClient, BeforeConnectHandler beforeConnect) - => new() { - Config = ipcClient.Config with { BeforeConnect = beforeConnect }, - Transport = ipcClient.Transport, - }; + ipcClient.BeforeConnect = beforeConnect; + return ipcClient; + } } \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs index 71fa5d83..e43eee21 100644 --- a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs +++ b/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs @@ -26,12 +26,10 @@ public async Task NamedPipesShoulNotLeak() private static IpcServer CreateServer(string pipeName) => new IpcServer { - Transport = [ - new NamedPipeServerTransport - { - PipeName = pipeName, - } - ], + Transport = new NamedPipeServerTransport + { + PipeName = pipeName, + }, Endpoints = new() { typeof(IComputingService) @@ -45,8 +43,7 @@ private static IpcServer CreateServer(string pipeName) private static IpcClient CreateClient(string pipeName) => new() { - Transport = new NamedPipeClientTransport { PipeName = pipeName }, - Config = new() + Transport = new NamedPipeClientTransport { PipeName = pipeName } }; private static Task ListPipes(string pattern) diff --git a/src/UiPath.Ipc.Tests/Program.cs b/src/UiPath.Ipc.Tests/Program.cs index 084096ce..feb7a67e 100644 --- a/src/UiPath.Ipc.Tests/Program.cs +++ b/src/UiPath.Ipc.Tests/Program.cs @@ -10,7 +10,7 @@ return 1; } var externalServerParams = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(base64))); -await using var asyncDisposable = externalServerParams.CreateListenerConfig(out var listener); +await using var asyncDisposable = externalServerParams.CreateListenerConfig(out var serverTransport); await using var serviceProvider = new ServiceCollection() .AddLogging(builder => builder.AddConsole()) @@ -25,7 +25,7 @@ { { typeof(IComputingService) }, }, - Transport = [listener], + Transport = serverTransport, }; ipcServer.Start(); await ipcServer.WaitForStop(); diff --git a/src/UiPath.Ipc.Tests/RobotTests.cs b/src/UiPath.Ipc.Tests/RobotTests.cs index c07902d8..627bb566 100644 --- a/src/UiPath.Ipc.Tests/RobotTests.cs +++ b/src/UiPath.Ipc.Tests/RobotTests.cs @@ -17,6 +17,10 @@ public abstract class RobotTests : TestBase protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; protected sealed override Type ContractType => typeof(IStudioOperations); + protected override EndpointCollection? Callbacks => new() + { + { typeof(IStudioEvents), _studioEvents } + }; protected readonly ConcurrentBag _clientBeforeCalls = new(); @@ -31,24 +35,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingleton() .AddSingletonAlias(); - protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) - => listener with - { - ConcurrentAccepts = 10, - RequestTimeout = Timeouts.DefaultRequest, - MaxReceivedMessageSizeInMegabytes = 1, - }; - protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) - => new() - { - RequestTimeout = Timeouts.DefaultRequest, - Scheduler = GuiScheduler, - Callbacks = callbacks ?? new() - { - { typeof(IStudioEvents), _studioEvents } - }, - BeforeCall = async (callInfo, _) => _clientBeforeCalls.Add(callInfo), - }; + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; #endregion [Fact] diff --git a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs index fed8d637..5e9b11eb 100644 --- a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs @@ -12,7 +12,7 @@ public sealed class RobotTestsOverNamedPipes : RobotTests public RobotTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected override async Task CreateListener() => new NamedPipeServerTransport + protected override async Task CreateServerTransport() => new NamedPipeServerTransport { PipeName = PipeName }; @@ -62,13 +62,8 @@ public static void OnConnectingToUserService() public TContract CreateUserServiceProxy(string pipeName) where TContract : class => RobotIpcHelpers.CreateProxy( - listener: new NamedPipeServerTransport() - { - PipeName = pipeName, - AccessControl = security => security.AllowCurrentUser(), - MaxReceivedMessageSizeInMegabytes = 10, - RequestTimeout = TimeSpan.FromSeconds(40), - }, + pipeName, + requestTimeout: TimeSpan.FromSeconds(40), callbacks: new EndpointCollection() { { typeof(TCallback), Instance } @@ -81,16 +76,6 @@ internal static partial class RobotIpcHelpers { private static readonly ConcurrentDictionary PipeClients = new(); - public static TContract CreateProxy( - NamedPipeServerTransport listener, - EndpointCollection? callbacks = null, - IServiceProvider? provider = null, - Action? beforeConnect = null, - BeforeCallHandler? beforeCall = null, - bool allowImpersonation = false, - TaskScheduler? scheduler = null) where TContract : class - => CreateProxy(listener.PipeName, listener.RequestTimeout, callbacks, provider, beforeConnect, beforeCall, allowImpersonation, scheduler); - public static TContract CreateProxy( string pipeName, TimeSpan? requestTimeout = null, @@ -101,6 +86,7 @@ public static TContract CreateProxy( 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. @@ -139,20 +125,17 @@ private static ClientAndParams CreateClient(CreateProxyRequest request) => new( Client: new() { - Config = 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 : _ => { - 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; - }, - BeforeCall = request.Params.BeforeCall, - Scheduler = request.Params.Scheduler, + request.Params.BeforeConnect(); + return Task.CompletedTask; }, + BeforeOutgoingCall = request.Params.BeforeCall, + Scheduler = request.Params.Scheduler, Transport = new NamedPipeClientTransport { PipeName = request.ActualKey.Name, diff --git a/src/UiPath.Ipc.Tests/SpyTestBase.cs b/src/UiPath.Ipc.Tests/SpyTestBase.cs new file mode 100644 index 00000000..2fc866f3 --- /dev/null +++ b/src/UiPath.Ipc.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.Ipc.Tests/SyncOverAsyncTests.cs b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs index 01d4ef79..6992eeeb 100644 --- a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs +++ b/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs @@ -47,12 +47,7 @@ public async Task RemoteCallingSyncOverAsync_IpcShouldBeResilient(ScenarioId sce private static IpcServer CreateServer(string pipeName) => new IpcServer { - Transport = [ - new NamedPipeServerTransport - { - PipeName = pipeName, - } - ], + Transport = new NamedPipeServerTransport { PipeName = pipeName }, Endpoints = new() { typeof(IComputingService) @@ -66,8 +61,7 @@ private static IpcServer CreateServer(string pipeName) private static IpcClient CreateClient(string pipeName) => new() { - Transport = new NamedPipeClientTransport { PipeName = pipeName }, - Config = new() + Transport = new NamedPipeClientTransport { PipeName = pipeName } }; diff --git a/src/UiPath.Ipc.Tests/SystemTests.cs b/src/UiPath.Ipc.Tests/SystemTests.cs index 73dc59a3..70db74df 100644 --- a/src/UiPath.Ipc.Tests/SystemTests.cs +++ b/src/UiPath.Ipc.Tests/SystemTests.cs @@ -30,20 +30,7 @@ protected override void ConfigureSpecificServices(IServiceCollection services) .AddSingleton() .AddSingletonAlias(); - protected override ServerTransport ConfigTransportAgnostic(ServerTransport listener) - => listener with - { - ConcurrentAccepts = 10, - RequestTimeout = Timeouts.DefaultRequest, - MaxReceivedMessageSizeInMegabytes = 1, - }; - protected override ClientConfig CreateClientConfig(EndpointCollection? callbacks = null) - => new() - { - RequestTimeout = Timeouts.DefaultRequest, - ServiceProvider = ServiceProvider, - Callbacks = callbacks - }; + protected override TimeSpan ServerRequestTimeout => Timeouts.DefaultRequest; #endregion [Theory, IpcAutoData] @@ -76,7 +63,7 @@ public async Task NotPassingAnOptionalMessage_ShouldWork() .ShouldBeAsync(true); [Fact] - [OverrideConfig(typeof(ServerExecutingTooLongACall_ShouldThrowTimeout_Config))] + [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() @@ -92,23 +79,24 @@ 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 ServerExecutingTooLongACall_ShouldThrowTimeout_Config : OverrideConfig + private sealed class ShortServerLongClientTimeout : OverrideConfig { - public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeouts.Short }; - public override IpcClient? Override(Func client) - => client().WithRequestTimeout(Timeout.InfiniteTimeSpan); + public override async Task Override(Func> ipcServerFactory) + { + var ipcServer = await ipcServerFactory(); + ipcServer.RequestTimeout = Timeouts.Short; + return ipcServer; + } + + public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeout.InfiniteTimeSpan); } private sealed class ClientWaitingForTooLongACall_ShouldThrowTimeout_Config : OverrideConfig { - public override async Task Override(Func> listener) => await listener() with { RequestTimeout = Timeout.InfiniteTimeSpan }; - public override IpcClient? Override(Func client) - => client().WithRequestTimeout(Timeouts.IpcRoundtrip); + public override Task Override(Func> ipcServerFactory) => ipcServerFactory().WithRequestTimeout(Timeout.InfiniteTimeSpan)!; + public override IpcClient? Override(Func client) => client().WithRequestTimeout(Timeouts.IpcRoundtrip); } - private ServerTransport ShortClientTimeout(ServerTransport listener) => listener with { RequestTimeout = TimeSpan.FromMilliseconds(100) }; - private ServerTransport InfiniteServerTimeout(ServerTransport listener) => listener with { RequestTimeout = Timeout.InfiniteTimeSpan }; - [Fact] public async Task FireAndForget_ShouldWork() { diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs index d0430161..27f83f70 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs @@ -9,7 +9,7 @@ public sealed class SystemTestsOverNamedPipes : SystemTests public SystemTestsOverNamedPipes(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected sealed override async Task CreateListener() => new NamedPipeServerTransport + protected sealed override async Task CreateServerTransport() => new NamedPipeServerTransport { PipeName = PipeName }; diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs index 265a9134..d9878b51 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs @@ -10,12 +10,12 @@ public sealed class SystemTestsOverTcp : SystemTests public SystemTestsOverTcp(ITestOutputHelper outputHelper) : base(outputHelper) { } - protected sealed override async Task CreateListener() + protected sealed override async Task CreateServerTransport() => new TcpServerTransport { EndPoint = _endPoint, }; protected override ClientTransport CreateClientTransport() - => new TcpTransport() { EndPoint = _endPoint }; + => new TcpClientTransport() { EndPoint = _endPoint }; } diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs index b8795dd2..bc17f6f6 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs +++ b/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs @@ -15,7 +15,7 @@ protected override async Task DisposeAsync() await base.DisposeAsync(); } - protected override async Task CreateListener() + protected override async Task CreateServerTransport() { var listener = new WebSocketServerTransport { @@ -27,5 +27,5 @@ protected override async Task CreateListener() } protected override ClientTransport CreateClientTransport() - => new WebSocketTransport() { Uri = _webSocketContext.ClientUri }; + => new WebSocketClientTransport() { Uri = _webSocketContext.ClientUri }; } diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.Ipc.Tests/TestBase.cs index 3086c9b3..5652726f 100644 --- a/src/UiPath.Ipc.Tests/TestBase.cs +++ b/src/UiPath.Ipc.Tests/TestBase.cs @@ -45,8 +45,8 @@ public TestBase(ITestOutputHelper outputHelper) _guiThread.SynchronizationContext.Send(() => Thread.CurrentThread.Name = Names.GuiThreadName); _serviceProvider = IpcHelpers.ConfigureServices(_outputHelper, ConfigureSpecificServices); - _ipcServer = new(CreateServer); - _ipcClient = new(() => CreateClient()); + _ipcServer = new(CreateIpcServer); + _ipcClient = new(() => CreateIpcClient()); OverrideConfig? GetOverrideConfig() { @@ -67,78 +67,84 @@ public TestBase(ITestOutputHelper outputHelper) protected abstract void ConfigureSpecificServices(IServiceCollection services); - private Task CreateListenerAndConfigure() - { - var factory = async () => - { - _outputHelper.WriteLine("Creating listener..."); - var listener = await CreateListener(); - listener = ConfigTransportAgnostic(listener); - return listener; - }; + protected virtual EndpointCollection? Callbacks => []; - if (_overrideConfig is null) + private Task CreateIpcServer() + { + if (_overrideConfig is not null) { - return factory(); + return _overrideConfig.Override(Core); } - return _overrideConfig.Override(factory); - } - - protected async Task CreateServer() - { - if (await CreateListenerAndConfigure() is not { } listener) return null; + return Core()!; - return new() + async Task Core() { - Endpoints = new() { - new EndpointSettings(ContractType) + _outputHelper.WriteLine($"Creating {nameof(ServerTransport)}..."); + + var serverTransport = await CreateServerTransport(); + ConfigTransportBase(serverTransport); + + var endpointSettings = new EndpointSettings(ContractType) + { + BeforeIncommingCall = (callInfo, ct) => { - BeforeCall = (callInfo, ct) => - { - _serverBeforeCalls.Add(callInfo); - return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask; - } + _serverBeforeCalls.Add(callInfo); + return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask; } - }, - Transport = [listener], - ServiceProvider = _serviceProvider, - Scheduler = GuiScheduler - }; - } + }; - protected IpcClient? CreateClient(EndpointCollection? callbacks = null) + return new() + { + Endpoints = new() { endpointSettings }, + Transport = serverTransport, + ServiceProvider = _serviceProvider, + Scheduler = GuiScheduler, + RequestTimeout = ServerRequestTimeout + }; + } + } + protected IpcClient? CreateIpcClient(EndpointCollection? callbacks = null) { - var factory = () => + if (_overrideConfig is null) + { + return CreateDefaultClient(); + } + + return _overrideConfig.Override(CreateDefaultClient); + + IpcClient CreateDefaultClient() { - var config = CreateClientConfig(callbacks); - var transport = CreateClientTransport(); var client = new IpcClient { - Config = config, - Transport = transport + Callbacks = callbacks ?? Callbacks, + Transport = CreateClientTransport() }; + ConfigureClient(client); return client; - }; - - if (_overrideConfig is null) - { - return factory(); } - - return _overrideConfig.Override(factory); } + protected TContract? GetProxy() where TContract : class => _ipcClient.Value?.GetProxy(); protected void CreateLazyProxy(out Lazy lazy) where TContract : class => lazy = new(GetProxy); - protected abstract Task CreateListener(); + protected abstract Task CreateServerTransport(); + protected abstract TimeSpan ServerRequestTimeout { get; } - protected abstract ClientConfig CreateClientConfig(EndpointCollection? callbacks = null); + protected virtual void ConfigureClient(IpcClient ipcClient) + { + ipcClient.RequestTimeout = Timeouts.DefaultRequest; + ipcClient.Scheduler = GuiScheduler; + } protected abstract ClientTransport CreateClientTransport(); - protected abstract ServerTransport ConfigTransportAgnostic(ServerTransport listener); + protected virtual void ConfigTransportBase(ServerTransport serverTransport) + { + serverTransport.ConcurrentAccepts = 10; + serverTransport.MaxReceivedMessageSizeInMegabytes = 1; + } protected virtual async Task DisposeAsync() { From 2799b98fb705c19cbf3f02f203901ad98ba11647 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 26 Nov 2024 10:20:17 +0100 Subject: [PATCH 05/26] taking shape --- src/UiPath.CoreIpc/Connection.cs | 4 +- src/UiPath.CoreIpc/Helpers/NestedStream.cs | 4 +- src/UiPath.CoreIpc/PublicAPI.Shipped.txt | 177 ++++++++++++++++++ src/UiPath.CoreIpc/PublicAPI.Unshipped.txt | 1 + src/UiPath.CoreIpc/Server/Server.cs | 2 +- .../UiPath.Ipc.net6.0-windows.received.txt | 135 +++++++------ 6 files changed, 247 insertions(+), 76 deletions(-) create mode 100644 src/UiPath.CoreIpc/PublicAPI.Shipped.txt create mode 100644 src/UiPath.CoreIpc/PublicAPI.Unshipped.txt diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index be2b3bde..3959f01e 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -364,7 +364,7 @@ private void OnCancellationReceived(CancellationRequest cancellationRequest) { try { - CancellationReceived(cancellationRequest.RequestId); + CancellationReceived?.Invoke(cancellationRequest.RequestId); } catch (Exception ex) { @@ -380,7 +380,7 @@ private async Task OnRequestReceivedAsyncSafe(Request request) { try { - await RequestReceived(request); + await (RequestReceived?.Invoke(request) ?? default); } catch (Exception ex) { diff --git a/src/UiPath.CoreIpc/Helpers/NestedStream.cs b/src/UiPath.CoreIpc/Helpers/NestedStream.cs index 2e69166e..a726678d 100644 --- a/src/UiPath.CoreIpc/Helpers/NestedStream.cs +++ b/src/UiPath.CoreIpc/Helpers/NestedStream.cs @@ -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/PublicAPI.Shipped.txt b/src/UiPath.CoreIpc/PublicAPI.Shipped.txt new file mode 100644 index 00000000..3a9b8f82 --- /dev/null +++ b/src/UiPath.CoreIpc/PublicAPI.Shipped.txt @@ -0,0 +1,177 @@ +#nullable enable +abstract UiPath.Ipc.ClientTransport.CreateState() -> UiPath.Ipc.IClientState! +abstract UiPath.Ipc.ClientTransport.Validate() -> void +abstract UiPath.Ipc.ServerTransport.CreateServerState() -> UiPath.Ipc.ServerTransport.IServerState! +abstract UiPath.Ipc.ServerTransport.ValidateCore() -> System.Collections.Generic.IEnumerable! +override UiPath.Ipc.EndpointSettings.WithServiceProvider(System.IServiceProvider? serviceProvider) -> UiPath.Ipc.EndpointSettings! +override UiPath.Ipc.Error.ToString() -> string! +override UiPath.Ipc.IpcProxy.Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) -> object? +override UiPath.Ipc.RemoteException.StackTrace.get -> string! +override UiPath.Ipc.RemoteException.ToString() -> string! +override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.CreateState() -> UiPath.Ipc.IClientState! +override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ToString() -> string! +override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.Validate() -> void +override UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ToString() -> string! +override UiPath.Ipc.Transport.Tcp.TcpClientTransport.CreateState() -> UiPath.Ipc.IClientState! +override UiPath.Ipc.Transport.Tcp.TcpClientTransport.ToString() -> string! +override UiPath.Ipc.Transport.Tcp.TcpClientTransport.Validate() -> void +override UiPath.Ipc.Transport.Tcp.TcpServerTransport.ToString() -> string! +override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.CreateState() -> UiPath.Ipc.IClientState! +override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.ToString() -> string! +override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Validate() -> void +override UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.ToString() -> string! +static UiPath.Ipc.Error.FromException(System.Exception? exception) -> UiPath.Ipc.Error? +static UiPath.Ipc.IOHelpers.Allow(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.IdentityReference! sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.Allow(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.AllowCurrentUser(this System.IO.Pipes.PipeSecurity! pipeSecurity, bool onlyNonAdmin = false) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.Deny(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.IdentityReference! sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.Deny(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.LocalOnly(this System.IO.Pipes.PipeSecurity! pipeSecurity) -> System.IO.Pipes.PipeSecurity! +static UiPath.Ipc.IOHelpers.PipeExists(string! pipeName, int timeout = 1) -> bool +static UiPath.Ipc.ServerTransport.IsNotNull(T? propertyValue, string? propertyName = null) -> string? +UiPath.Ipc.CallInfo +UiPath.Ipc.CallInfo.Arguments.get -> object?[]! +UiPath.Ipc.CallInfo.CallInfo() -> void +UiPath.Ipc.CallInfo.CallInfo(bool newConnection, System.Reflection.MethodInfo! method, object?[]! arguments) -> void +UiPath.Ipc.CallInfo.Method.get -> System.Reflection.MethodInfo! +UiPath.Ipc.CallInfo.NewConnection.get -> bool +UiPath.Ipc.ClientTransport +UiPath.Ipc.EndpointCollection +UiPath.Ipc.EndpointCollection.Add(System.Type! contractType, object? instance) -> void +UiPath.Ipc.EndpointCollection.Add(System.Type! type) -> void +UiPath.Ipc.EndpointCollection.Add(UiPath.Ipc.EndpointSettings! endpointSettings) -> void +UiPath.Ipc.EndpointCollection.EndpointCollection() -> void +UiPath.Ipc.EndpointCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +UiPath.Ipc.EndpointNotFoundException +UiPath.Ipc.EndpointNotFoundException.EndpointName.get -> string! +UiPath.Ipc.EndpointNotFoundException.EndpointNotFoundException(string! paramName, string! serverDebugName, string! endpointName) -> void +UiPath.Ipc.EndpointNotFoundException.ServerDebugName.get -> string! +UiPath.Ipc.EndpointSettings +UiPath.Ipc.EndpointSettings.BeforeIncommingCall.get -> System.Func? +UiPath.Ipc.EndpointSettings.BeforeIncommingCall.set -> void +UiPath.Ipc.EndpointSettings.ContractType.get -> System.Type! +UiPath.Ipc.EndpointSettings.EndpointSettings(System.Type! contractType, object? serviceInstance = null) -> void +UiPath.Ipc.EndpointSettings.EndpointSettings(System.Type! contractType, System.IServiceProvider! serviceProvider) -> void +UiPath.Ipc.EndpointSettings.Scheduler.get -> System.Threading.Tasks.TaskScheduler? +UiPath.Ipc.EndpointSettings.Scheduler.set -> void +UiPath.Ipc.EndpointSettings.ServiceInstance.get -> object? +UiPath.Ipc.EndpointSettings.ServiceProvider.get -> System.IServiceProvider? +UiPath.Ipc.EndpointSettings.Validate() -> void +UiPath.Ipc.EndpointSettings +UiPath.Ipc.EndpointSettings.EndpointSettings(System.IServiceProvider! serviceProvider) -> void +UiPath.Ipc.EndpointSettings.EndpointSettings(TContract? serviceInstance = null) -> void +UiPath.Ipc.Error +UiPath.Ipc.Error.Error(string! Message, string! StackTrace, string! Type, UiPath.Ipc.Error? InnerError) -> void +UiPath.Ipc.Error.InnerError.get -> UiPath.Ipc.Error? +UiPath.Ipc.Error.InnerError.init -> void +UiPath.Ipc.Error.Message.get -> string! +UiPath.Ipc.Error.Message.init -> void +UiPath.Ipc.Error.StackTrace.get -> string! +UiPath.Ipc.Error.StackTrace.init -> void +UiPath.Ipc.Error.Type.get -> string! +UiPath.Ipc.Error.Type.init -> void +UiPath.Ipc.IClient +UiPath.Ipc.IClient.GetCallback() -> TCallbackInterface! +UiPath.Ipc.IClient.Impersonate(System.Action! action) -> void +UiPath.Ipc.IClientState +UiPath.Ipc.IClientState.Connect(UiPath.Ipc.IpcClient! client, System.Threading.CancellationToken ct) -> System.Threading.Tasks.ValueTask +UiPath.Ipc.IClientState.IsConnected() -> bool +UiPath.Ipc.IClientState.Network.get -> System.IO.Stream? +UiPath.Ipc.IOHelpers +UiPath.Ipc.IpcClient +UiPath.Ipc.IpcClient.BeforeConnect.get -> System.Func? +UiPath.Ipc.IpcClient.BeforeConnect.set -> void +UiPath.Ipc.IpcClient.BeforeOutgoingCall.get -> System.Func? +UiPath.Ipc.IpcClient.BeforeOutgoingCall.set -> void +UiPath.Ipc.IpcClient.Callbacks.get -> UiPath.Ipc.EndpointCollection? +UiPath.Ipc.IpcClient.Callbacks.set -> void +UiPath.Ipc.IpcClient.DebugName.get -> string! +UiPath.Ipc.IpcClient.DebugName.set -> void +UiPath.Ipc.IpcClient.GetProxy() -> TProxy! +UiPath.Ipc.IpcClient.IpcClient() -> void +UiPath.Ipc.IpcClient.Logger.get -> Microsoft.Extensions.Logging.ILogger? +UiPath.Ipc.IpcClient.Logger.init -> void +UiPath.Ipc.IpcClient.Transport.get -> UiPath.Ipc.ClientTransport! +UiPath.Ipc.IpcClient.Transport.init -> void +UiPath.Ipc.IpcProxy +UiPath.Ipc.IpcProxy.CloseConnection() -> System.Threading.Tasks.ValueTask +UiPath.Ipc.IpcProxy.ConnectionClosed -> System.EventHandler! +UiPath.Ipc.IpcProxy.Dispose() -> void +UiPath.Ipc.IpcProxy.IpcProxy() -> void +UiPath.Ipc.IpcProxy.Network.get -> System.IO.Stream? +UiPath.Ipc.IpcServer +UiPath.Ipc.IpcServer.DisposeAsync() -> System.Threading.Tasks.ValueTask +UiPath.Ipc.IpcServer.Endpoints.get -> UiPath.Ipc.EndpointCollection! +UiPath.Ipc.IpcServer.Endpoints.init -> void +UiPath.Ipc.IpcServer.IpcServer() -> void +UiPath.Ipc.IpcServer.Start() -> void +UiPath.Ipc.IpcServer.Transport.get -> UiPath.Ipc.ServerTransport! +UiPath.Ipc.IpcServer.Transport.init -> void +UiPath.Ipc.IpcServer.WaitForStart() -> System.Threading.Tasks.Task! +UiPath.Ipc.IpcServer.WaitForStop() -> System.Threading.Tasks.Task! +UiPath.Ipc.Message +UiPath.Ipc.Message.Client.get -> UiPath.Ipc.IClient! +UiPath.Ipc.Message.Client.set -> void +UiPath.Ipc.Message.GetCallback() -> TCallbackInterface! +UiPath.Ipc.Message.ImpersonateClient(System.Action! action) -> void +UiPath.Ipc.Message.Message() -> void +UiPath.Ipc.Message.RequestTimeout.get -> System.TimeSpan +UiPath.Ipc.Message.RequestTimeout.set -> void +UiPath.Ipc.Message +UiPath.Ipc.Message.Message(TPayload payload) -> void +UiPath.Ipc.Message.Payload.get -> TPayload +UiPath.Ipc.Peer +UiPath.Ipc.Peer.Peer() -> void +UiPath.Ipc.Peer.RequestTimeout.get -> System.TimeSpan +UiPath.Ipc.Peer.RequestTimeout.set -> void +UiPath.Ipc.Peer.Scheduler.get -> System.Threading.Tasks.TaskScheduler? +UiPath.Ipc.Peer.Scheduler.set -> void +UiPath.Ipc.Peer.ServiceProvider.get -> System.IServiceProvider? +UiPath.Ipc.Peer.ServiceProvider.set -> void +UiPath.Ipc.RemoteException +UiPath.Ipc.RemoteException.InnerException.get -> UiPath.Ipc.RemoteException? +UiPath.Ipc.RemoteException.Is() -> bool +UiPath.Ipc.RemoteException.RemoteException(UiPath.Ipc.Error! error) -> void +UiPath.Ipc.RemoteException.Type.get -> string! +UiPath.Ipc.ServerTransport +UiPath.Ipc.ServerTransport.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate? +UiPath.Ipc.ServerTransport.Certificate.init -> void +UiPath.Ipc.ServerTransport.ConcurrentAccepts.get -> int +UiPath.Ipc.ServerTransport.ConcurrentAccepts.set -> void +UiPath.Ipc.ServerTransport.IServerConnectionSlot +UiPath.Ipc.ServerTransport.IServerConnectionSlot.AwaitConnection(System.Threading.CancellationToken ct) -> System.Threading.Tasks.ValueTask +UiPath.Ipc.ServerTransport.IServerState +UiPath.Ipc.ServerTransport.IServerState.CreateConnectionSlot() -> UiPath.Ipc.ServerTransport.IServerConnectionSlot! +UiPath.Ipc.ServerTransport.MaxReceivedMessageSizeInMegabytes.get -> byte +UiPath.Ipc.ServerTransport.MaxReceivedMessageSizeInMegabytes.set -> void +UiPath.Ipc.ServerTransport.ServerTransport() -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.AllowImpersonation.get -> bool +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.AllowImpersonation.init -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.PipeName.get -> string! +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.PipeName.init -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ServerName.get -> string! +UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ServerName.init -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.AccessControl.get -> System.Action? +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.AccessControl.init -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.NamedPipeServerTransport() -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.PipeName.get -> string! +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.PipeName.init -> void +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ServerName.get -> string! +UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ServerName.init -> void +UiPath.Ipc.Transport.Tcp.TcpClientTransport +UiPath.Ipc.Transport.Tcp.TcpClientTransport.EndPoint.get -> System.Net.IPEndPoint! +UiPath.Ipc.Transport.Tcp.TcpClientTransport.EndPoint.init -> void +UiPath.Ipc.Transport.Tcp.TcpServerTransport +UiPath.Ipc.Transport.Tcp.TcpServerTransport.EndPoint.get -> System.Net.IPEndPoint! +UiPath.Ipc.Transport.Tcp.TcpServerTransport.EndPoint.init -> void +UiPath.Ipc.Transport.Tcp.TcpServerTransport.TcpServerTransport() -> void +UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport +UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Uri.get -> System.Uri! +UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Uri.init -> void +UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport +UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.Accept.get -> System.Func!>! +UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.Accept.init -> void +UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.WebSocketServerTransport() -> void +virtual UiPath.Ipc.EndpointSettings.WithServiceProvider(System.IServiceProvider? serviceProvider) -> UiPath.Ipc.EndpointSettings! \ No newline at end of file diff --git a/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt b/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index 16b1e691..e49f8164 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -226,7 +226,7 @@ void Deserialize() } if (argument is Message message) { - message.Client = _client; + message.Client = _client!; } return argument; } 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 index c99c698a..a866328c 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -16,18 +16,6 @@ namespace UiPath.Ipc public System.Reflection.MethodInfo Method { get; } public bool NewConnection { get; } } - public sealed class ClientConfig : UiPath.Ipc.EndpointConfig, System.IEquatable - { - public ClientConfig() { } - public string DebugName { get; set; } - public UiPath.Ipc.ISerializer? Serializer { get; set; } - public System.Func? BeforeCall { get; init; } - public System.Func? BeforeConnect { get; init; } - public UiPath.Ipc.EndpointCollection? Callbacks { get; init; } - public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } - public System.Threading.Tasks.TaskScheduler? Scheduler { get; init; } - public System.IServiceProvider? ServiceProvider { get; init; } - } public abstract class ClientTransport : System.IEquatable { protected ClientTransport() { } @@ -42,11 +30,6 @@ namespace UiPath.Ipc public void Add(System.Type contractType, object? instance) { } public System.Collections.Generic.IEnumerator GetEnumerator() { } } - public abstract class EndpointConfig : System.IEquatable - { - protected EndpointConfig() { } - public System.TimeSpan RequestTimeout { get; init; } - } [System.Serializable] public sealed class EndpointNotFoundException : System.ArgumentOutOfRangeException { @@ -58,7 +41,7 @@ namespace UiPath.Ipc { public EndpointSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } public EndpointSettings(System.Type contractType, object? serviceInstance = null) { } - public System.Func? BeforeCall { get; set; } + public System.Func? BeforeIncommingCall { get; set; } public System.Type ContractType { get; } public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } public object? ServiceInstance { get; } @@ -107,19 +90,16 @@ namespace UiPath.Ipc public static System.IO.Pipes.PipeSecurity LocalOnly(this System.IO.Pipes.PipeSecurity pipeSecurity) { } public static bool PipeExists(string pipeName, int timeout = 1) { } } - public interface ISerializer - { - object? Deserialize(string json, System.Type type); - System.Threading.Tasks.ValueTask DeserializeAsync(System.IO.Stream json, Microsoft.Extensions.Logging.ILogger? logger); - string Serialize(object? obj); - void Serialize(object? obj, System.IO.Stream stream); - } - public sealed class IpcClient + public sealed class IpcClient : UiPath.Ipc.Peer { [System.Obsolete("Constructors of types with required members are not supported in this version of " + "your compiler.", true)] public IpcClient() { } - public UiPath.Ipc.ClientConfig Config { get; init; } + public System.Func? BeforeConnect { get; set; } + public System.Func? BeforeOutgoingCall { get; set; } + public UiPath.Ipc.EndpointCollection? Callbacks { get; set; } + public string DebugName { get; set; } + public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } public UiPath.Ipc.ClientTransport Transport { get; init; } public TProxy GetProxy() where TProxy : class { } @@ -133,27 +113,21 @@ namespace UiPath.Ipc public void Dispose() { } protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) { } } - public sealed class IpcServer : System.IAsyncDisposable + public sealed class IpcServer : UiPath.Ipc.Peer, 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.EndpointCollection Endpoints { get; init; } - public System.Collections.Generic.IReadOnlyList Listeners { get; init; } - public System.Threading.Tasks.TaskScheduler? Scheduler { get; init; } - public System.IServiceProvider ServiceProvider { get; init; } + public UiPath.Ipc.ServerTransport Transport { get; init; } public System.Threading.Tasks.ValueTask DisposeAsync() { } + [System.Diagnostics.CodeAnalysis.MemberNotNull(new string[] { + "Transport", + "_accepter"})] public void Start() { } public System.Threading.Tasks.Task WaitForStart() { } public System.Threading.Tasks.Task WaitForStop() { } } - public abstract class ListenerConfig : UiPath.Ipc.EndpointConfig, System.IEquatable - { - protected ListenerConfig() { } - public System.Security.Cryptography.X509Certificates.X509Certificate? Certificate { get; init; } - public int ConcurrentAccepts { get; init; } - public byte MaxReceivedMessageSizeInMegabytes { get; init; } - } public class Message { public Message() { } @@ -170,6 +144,13 @@ namespace UiPath.Ipc public Message(TPayload payload) { } public TPayload Payload { get; } } + public abstract class Peer + { + protected Peer() { } + public System.TimeSpan RequestTimeout { get; set; } + public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } + public System.IServiceProvider? ServiceProvider { get; set; } + } [System.Serializable] public class RemoteException : System.Exception { @@ -181,84 +162,96 @@ namespace UiPath.Ipc where TException : System.Exception { } public override string ToString() { } } -} -namespace UiPath.Ipc.Extensibility -{ - public interface IListenerConfig - where TSelf : UiPath.Ipc.ListenerConfig, UiPath.Ipc.Extensibility.IListenerConfig - where TListenerState : System.IAsyncDisposable + public abstract class ServerTransport { - System.Threading.Tasks.ValueTask AwaitConnection(TListenerState listenerState, TConnectionState connectionState, System.Threading.CancellationToken ct); - TConnectionState CreateConnectionState(UiPath.Ipc.IpcServer server, TListenerState listenerState); - TListenerState CreateListenerState(UiPath.Ipc.IpcServer server); - System.Collections.Generic.IEnumerable Validate(); + protected ServerTransport() { } + public int ConcurrentAccepts { get; set; } + public byte MaxReceivedMessageSizeInMegabytes { get; set; } + public System.Security.Cryptography.X509Certificates.X509Certificate? Certificate { get; init; } + protected abstract UiPath.Ipc.ServerTransport.IServerState CreateServerState(); + protected abstract System.Collections.Generic.IEnumerable ValidateCore(); + protected static string? IsNotNull(T? propertyValue, [System.Runtime.CompilerServices.CallerArgumentExpression("propertyValue")] string? propertyName = null) { } + protected interface IServerConnectionSlot : System.IDisposable + { + System.Threading.Tasks.ValueTask AwaitConnection(System.Threading.CancellationToken ct); + } + protected interface IServerState : System.IAsyncDisposable + { + UiPath.Ipc.ServerTransport.IServerConnectionSlot CreateConnectionSlot(); + } } } namespace UiPath.Ipc.Transport.NamedPipe { - public sealed class NamedPipeListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + 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 NamedPipeListener() { } - [Newtonsoft.Json.JsonIgnore] - public System.Action? AccessControl { get; init; } + public NamedPipeClientTransport() { } + public bool AllowImpersonation { get; init; } public string PipeName { get; init; } public string ServerName { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } + public override void Validate() { } } - public sealed class NamedPipeTransport : UiPath.Ipc.ClientTransport, System.IEquatable + 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 NamedPipeTransport() { } - public bool AllowImpersonation { get; init; } + public NamedPipeServerTransport() { } + [Newtonsoft.Json.JsonIgnore] + public System.Action? AccessControl { get; init; } public string PipeName { get; init; } public string ServerName { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } + protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - public override void Validate() { } + protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } namespace UiPath.Ipc.Transport.Tcp { - public sealed class TcpListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + 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 TcpListener() { } + public TcpClientTransport() { } public System.Net.IPEndPoint EndPoint { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } + public override void Validate() { } } - public sealed class TcpTransport : UiPath.Ipc.ClientTransport, System.IEquatable + 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 TcpTransport() { } + public TcpServerTransport() { } public System.Net.IPEndPoint EndPoint { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } + protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - public override void Validate() { } + protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } namespace UiPath.Ipc.Transport.WebSocket { - public sealed class WebSocketListener : UiPath.Ipc.ListenerConfig, System.IEquatable, UiPath.Ipc.Extensibility.IListenerConfig + 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 WebSocketListener() { } - public System.Func> Accept { get; init; } + public WebSocketClientTransport() { } + public System.Uri Uri { get; init; } + public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } + public override void Validate() { } } - public sealed class WebSocketTransport : UiPath.Ipc.ClientTransport, System.IEquatable + public sealed class WebSocketServerTransport : UiPath.Ipc.ServerTransport, System.IAsyncDisposable, System.IDisposable, UiPath.Ipc.ServerTransport.IServerConnectionSlot, UiPath.Ipc.ServerTransport.IServerState { [System.Obsolete("Constructors of types with required members are not supported in this version of " + "your compiler.", true)] - public WebSocketTransport() { } - public System.Uri Uri { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } + public WebSocketServerTransport() { } + public System.Func> Accept { get; init; } + protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - public override void Validate() { } + protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } \ No newline at end of file From 8d6b388e8820319cc9b8ce8f65597a638fcb4cf6 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 26 Nov 2024 14:45:22 +0100 Subject: [PATCH 06/26] taking shape --- src/CoreIpc.sln | 12 +- src/Playground/Playground.csproj | 2 +- src/Playground/Program.cs | 2 +- .../ServerTransportBase.cs | 26 ++++ .../BidiHttpServerTransport.cs | 124 +++++++++--------- .../Constants.cs | 2 +- .../GlobalUsings.cs | 1 + ...reIpc.Extensions.BidirectionalHttp.csproj} | 0 src/UiPath.CoreIpc.Http/GlobalUsings.cs | 1 - src/UiPath.CoreIpc/Config/ClientTransport.cs | 4 +- src/UiPath.CoreIpc/Config/IClientState.cs | 2 +- src/UiPath.CoreIpc/Config/IListenerConfig.cs | 11 -- .../Config/{Peer.cs => IpcBase.cs} | 2 +- src/UiPath.CoreIpc/Config/IpcClient.cs | 4 +- src/UiPath.CoreIpc/Config/IpcServer.cs | 4 +- src/UiPath.CoreIpc/Config/ServerTransport.cs | 14 +- src/UiPath.CoreIpc/GlobalUsings.cs | 3 +- src/UiPath.CoreIpc/Helpers/Router.cs | 2 +- src/UiPath.CoreIpc/Server/EndpointSettings.cs | 2 +- .../Server/ServerTransportRunner.cs | 10 -- .../NamedPipe/NamedPipeClientTransport.cs | 4 +- .../NamedPipe/NamedPipeServerTransport.cs | 14 +- .../Transport/Tcp/TcpClientTransport.cs | 4 +- .../Transport/Tcp/TcpServerTransport.cs | 6 +- .../WebSocket/WebSocketClientTransport.cs | 4 +- .../WebSocket/WebSocketServerTransport.cs | 32 +++-- src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 5 +- .../UiPath.Ipc.net6.0-windows.received.txt | 56 ++------ src/UiPath.Ipc.Tests/TestBase.cs | 2 +- src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj | 2 +- 30 files changed, 175 insertions(+), 182 deletions(-) create mode 100644 src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs rename src/{UiPath.CoreIpc.Http => UiPath.CoreIpc.Extensions.BidirectionalHttp}/BidiHttpServerTransport.cs (66%) rename src/{UiPath.CoreIpc.Http => UiPath.CoreIpc.Extensions.BidirectionalHttp}/Constants.cs (76%) create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs rename src/{UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj => UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj} (100%) delete mode 100644 src/UiPath.CoreIpc.Http/GlobalUsings.cs delete mode 100644 src/UiPath.CoreIpc/Config/IListenerConfig.cs rename src/UiPath.CoreIpc/Config/{Peer.cs => IpcBase.cs} (87%) delete mode 100644 src/UiPath.CoreIpc/Server/ServerTransportRunner.cs diff --git a/src/CoreIpc.sln b/src/CoreIpc.sln index 74779ed7..2d716dc9 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -13,12 +13,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "Playground\Playground.csproj", "{F0365E40-DA73-4583-A363-89CBEF68A4C6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc.Http", "UiPath.CoreIpc.Http\UiPath.CoreIpc.Http.csproj", "{8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.Ipc.Tests", "UiPath.Ipc.Tests\UiPath.Ipc.Tests.csproj", "{E238E183-92CF-48A6-890F-C422853D6656}" 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,10 +33,6 @@ Global {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 - {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8776E55A-D4EB-4C3A-8FA2-29E9A1CAE469}.Release|Any CPU.Build.0 = Release|Any CPU {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.Build.0 = Debug|Any CPU {E238E183-92CF-48A6-890F-C422853D6656}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -45,6 +41,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Playground/Playground.csproj b/src/Playground/Playground.csproj index 334c100b..e6a9a7fb 100644 --- a/src/Playground/Playground.csproj +++ b/src/Playground/Playground.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index e373bd18..fbfeed13 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -44,7 +44,7 @@ private static async Task Main(string[] args) typeof(Contracts.IServerOperations), // DEVINE new EndpointSettings(typeof(Contracts.IServerOperations)) // ASTALALT { - BeforeIncommingCall = async (callInfo, _) => + BeforeIncomingCall = async (callInfo, _) => { Console.WriteLine($"Server: {callInfo.Method.Name}"); } diff --git a/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs new file mode 100644 index 00000000..6fd88769 --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs @@ -0,0 +1,26 @@ +using UiPath.Ipc; + +namespace UiPath.CoreIpc.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.Http/BidiHttpServerTransport.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs similarity index 66% rename from src/UiPath.CoreIpc.Http/BidiHttpServerTransport.cs rename to src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs index 4e43ce58..7b536b1d 100644 --- a/src/UiPath.CoreIpc.Http/BidiHttpServerTransport.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs @@ -7,20 +7,20 @@ using System.Net.Http; using System.Threading.Channels; -namespace UiPath.Ipc.Http; +namespace UiPath.CoreIpc.Extensions.BidirectionalHttp; using static Constants; -public sealed partial class BidiHttpServerTransport : ServerTransport +public sealed partial class BidiHttpServerTransport : ServerTransportBase { public required Uri Uri { get; init; } - protected override IServerState CreateServerState() + protected override ServerState CreateState() => new BidiHttpServerState(this); - protected override IEnumerable ValidateCore() => []; + protected override IEnumerable Validate() => []; - private sealed class BidiHttpServerState : IServerState + private sealed class BidiHttpServerState : ServerState { private readonly CancellationTokenSource _cts = new(); private readonly HttpListener _httpListener; @@ -46,7 +46,7 @@ public BidiHttpServerState(BidiHttpServerTransport transport) _disposing = new(DisposeCore); } - public ValueTask DisposeAsync() => new(_disposing.Value); + public override ValueTask DisposeAsync() => new(_disposing.Value); private async Task DisposeCore() { @@ -110,34 +110,27 @@ bool TryAcceptContext(HttpListenerContext context, out Guid connectionId, [NotNu } } - IServerConnectionSlot IServerState.CreateConnectionSlot() => new BidiHttpServerConnectionSlot(this); + public override ServerConnectionSlot CreateServerConnectionSlot() => new BidiHttpServerConnectionSlot(this); + } - private sealed class BidiHttpServerConnectionSlot : Stream, IServerConnectionSlot, IAsyncDisposable + private sealed class BidiHttpServerConnectionSlot : ServerConnectionSlot { private readonly Pipe _pipe = new(); - - private readonly BidiHttpServerState _listenerState; - + 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; - private readonly Lazy _disposing; public BidiHttpServerConnectionSlot(BidiHttpServerState serverState) { - _listenerState = serverState; + _serverState = serverState; _disposing = new(DisposeCore); } - public -#if !NET461 - override -#endif - ValueTask DisposeAsync() => new(_disposing.Value); - private async Task DisposeCore() { _cts.Cancel(); @@ -156,7 +149,7 @@ private async Task DisposeCore() _cts.Dispose(); } - public async Task WaitForConnection(CancellationToken ct) + public override async ValueTask AwaitConnection(CancellationToken ct) { using (await _lock.LockAsync(ct)) { @@ -165,24 +158,28 @@ public async Task WaitForConnection(CancellationToken ct) throw new InvalidOperationException(); } - _connection = await _listenerState.NewConnections.ReadAsync(ct); + _connection = await _serverState.NewConnections.ReadAsync(ct); _client = new() { BaseAddress = _connection.Value.reverseUri, DefaultRequestHeaders = - { - { ConnectionIdHeader, _connection.Value.connectionId.ToString() } - } + { + { 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 = _listenerState.GetConnectionChannel(_connection!.Value.connectionId); + var reader = _serverState.GetConnectionChannel(_connection!.Value.connectionId); while (await reader.WaitToReadAsync(ct)) { @@ -221,58 +218,67 @@ async Task ProcessContext(HttpListenerContext context) } } - ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) + private sealed class Adapter : Stream { - throw new NotImplementedException(); - } + 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 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 _pipe.Reader.ReadAsync(ct); + 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); + var take = (int)Math.Min(readResult.Buffer.Length, memory.Length); - readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); - _pipe.Reader.AdvanceTo(readResult.Buffer.GetPosition(take)); + readResult.Buffer.Slice(start: 0, length: take).CopyTo(memory.Span); + _slot._pipe.Reader.AdvanceTo(readResult.Buffer.GetPosition(take)); - return take; - } + return take; + } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) - { - var memory = new ReadOnlyMemory(buffer, offset, count); - if (_client is null) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) { - throw new InvalidOperationException(); - } + var memory = new ReadOnlyMemory(buffer, offset, count); + if (_slot._client is null) + { + throw new InvalidOperationException(); + } - HttpContent content = + HttpContent content = #if NET461 new ByteArrayContent(memory.ToArray()); #else - new ReadOnlyMemoryContent(memory); + new ReadOnlyMemoryContent(memory); #endif - await _client.PostAsync(requestUri: "", content, ct); - } + await _slot._client.PostAsync(requestUri: "", content, ct); + } - public override Task FlushAsync(CancellationToken cancellationToken) - => Task.CompletedTask; + 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 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(); } + 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.Http/Constants.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs similarity index 76% rename from src/UiPath.CoreIpc.Http/Constants.cs rename to src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs index c04e37b7..43abe56d 100644 --- a/src/UiPath.CoreIpc.Http/Constants.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Http; +namespace UiPath.CoreIpc.Extensions.BidirectionalHttp; internal static class Constants { diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs new file mode 100644 index 00000000..d502d37a --- /dev/null +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs @@ -0,0 +1 @@ +global using UiPath.CoreIpc.Extensions.Abstractions; diff --git a/src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj similarity index 100% rename from src/UiPath.CoreIpc.Http/UiPath.CoreIpc.Http.csproj rename to src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj diff --git a/src/UiPath.CoreIpc.Http/GlobalUsings.cs b/src/UiPath.CoreIpc.Http/GlobalUsings.cs deleted file mode 100644 index a7770972..00000000 --- a/src/UiPath.CoreIpc.Http/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using UiPath.Ipc.Extensibility; diff --git a/src/UiPath.CoreIpc/Config/ClientTransport.cs b/src/UiPath.CoreIpc/Config/ClientTransport.cs index 2e21728a..221c16e3 100644 --- a/src/UiPath.CoreIpc/Config/ClientTransport.cs +++ b/src/UiPath.CoreIpc/Config/ClientTransport.cs @@ -2,6 +2,6 @@ public abstract record ClientTransport { - public abstract IClientState CreateState(); - public abstract void Validate(); + internal abstract IClientState CreateState(); + internal abstract void Validate(); } diff --git a/src/UiPath.CoreIpc/Config/IClientState.cs b/src/UiPath.CoreIpc/Config/IClientState.cs index 74ff640c..a09230cf 100644 --- a/src/UiPath.CoreIpc/Config/IClientState.cs +++ b/src/UiPath.CoreIpc/Config/IClientState.cs @@ -1,6 +1,6 @@ namespace UiPath.Ipc; -public interface IClientState : IDisposable +internal interface IClientState : IDisposable { Stream? Network { get; } diff --git a/src/UiPath.CoreIpc/Config/IListenerConfig.cs b/src/UiPath.CoreIpc/Config/IListenerConfig.cs deleted file mode 100644 index da4732fc..00000000 --- a/src/UiPath.CoreIpc/Config/IListenerConfig.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace UiPath.Ipc.Extensibility; - -internal interface IListenerConfig - where TSelf : ServerTransport, IListenerConfig - where TListenerState : IAsyncDisposable -{ - TListenerState CreateListenerState(IpcServer server); - TConnectionState CreateConnectionState(IpcServer server, TListenerState listenerState); - ValueTask AwaitConnection(TListenerState listenerState, TConnectionState connectionState, CancellationToken ct); - IEnumerable Validate(); -} diff --git a/src/UiPath.CoreIpc/Config/Peer.cs b/src/UiPath.CoreIpc/Config/IpcBase.cs similarity index 87% rename from src/UiPath.CoreIpc/Config/Peer.cs rename to src/UiPath.CoreIpc/Config/IpcBase.cs index 4d4f7121..1c03b561 100644 --- a/src/UiPath.CoreIpc/Config/Peer.cs +++ b/src/UiPath.CoreIpc/Config/IpcBase.cs @@ -1,6 +1,6 @@ namespace UiPath.Ipc; -public abstract class Peer +public abstract class IpcBase { public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; public IServiceProvider? ServiceProvider { get; set; } diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index 7bed1c05..1b95fd3a 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -public sealed class IpcClient : Peer, IClientConfig +public sealed class IpcClient : IpcBase, IClientConfig { public EndpointCollection? Callbacks { get; set; } @@ -63,7 +63,7 @@ internal RouterConfig CreateCallbackRouterConfig() Callbacks.OrDefault(), endpoint => endpoint with { - BeforeIncommingCall = null, // callbacks don't support BeforeCall + BeforeIncomingCall = null, // callbacks don't support BeforeCall Scheduler = endpoint.Scheduler ?? Scheduler }); } diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index e9c3d14d..da5083db 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -public sealed class IpcServer : Peer, IAsyncDisposable +public sealed class IpcServer : IpcBase, IAsyncDisposable { public required EndpointCollection Endpoints { get; init; } public required ServerTransport Transport { get; init; } @@ -163,7 +163,7 @@ private async Task Accept(CancellationToken ct) } catch (Exception ex) { - slot.Dispose(); + await slot.DisposeAsync(); _newConnection.OnError(ex); return; } diff --git a/src/UiPath.CoreIpc/Config/ServerTransport.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs index c63bc155..1d0c27c8 100644 --- a/src/UiPath.CoreIpc/Config/ServerTransport.cs +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -8,7 +8,9 @@ public abstract class ServerTransport public int ConcurrentAccepts { get; set; } = 5; public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; - public X509Certificate? Certificate { get; init; } + // TODO: Will be decommissioned altogether. + internal X509Certificate? Certificate { get; init; } + internal int MaxMessageSize => MaxReceivedMessageSizeInMegabytes * 1024 * 1024; // TODO: Maybe decommission. @@ -34,12 +36,12 @@ internal async Task MaybeAuthenticate(Stream network) return sslStream; } - protected internal abstract IServerState CreateServerState(); + internal abstract IServerState CreateServerState(); internal IEnumerable Validate() => ValidateCore().Where(x => x is not null).Select(x => $"{GetType().Name}.{x}"); - protected abstract IEnumerable ValidateCore(); - protected static string? IsNotNull(T? propertyValue, [CallerArgumentExpression(nameof(propertyValue))] string? propertyName = null) + internal abstract IEnumerable ValidateCore(); + internal static string? IsNotNull(T? propertyValue, [CallerArgumentExpression(nameof(propertyValue))] string? propertyName = null) { if (propertyValue is null) { @@ -48,11 +50,11 @@ internal IEnumerable Validate() return null; } - protected internal interface IServerState : IAsyncDisposable + internal interface IServerState : IAsyncDisposable { IServerConnectionSlot CreateConnectionSlot(); } - protected internal interface IServerConnectionSlot : IDisposable + internal interface IServerConnectionSlot : IAsyncDisposable { ValueTask AwaitConnection(CancellationToken ct); } diff --git a/src/UiPath.CoreIpc/GlobalUsings.cs b/src/UiPath.CoreIpc/GlobalUsings.cs index ab295693..00369edd 100644 --- a/src/UiPath.CoreIpc/GlobalUsings.cs +++ b/src/UiPath.CoreIpc/GlobalUsings.cs @@ -1,5 +1,4 @@ -global using UiPath.Ipc.Extensibility; -global using BeforeConnectHandler = System.Func; +global using BeforeConnectHandler = System.Func; global using BeforeCallHandler = System.Func; global using InvokeDelegate = System.Func; global using Accept = System.Func>; diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs index 26684819..7906f013 100644 --- a/src/UiPath.CoreIpc/Helpers/Router.cs +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -131,7 +131,7 @@ public static Route From(IServiceProvider? serviceProvider, EndpointSettings end => new Route() { Service = endpointSettings.Service.WithProvider(serviceProvider), - BeforeCall = endpointSettings.BeforeIncommingCall, + BeforeCall = endpointSettings.BeforeIncomingCall, Scheduler = endpointSettings.Scheduler.OrDefault(), LoggerFactory = serviceProvider.MaybeCreateServiceFactory(), }; diff --git a/src/UiPath.CoreIpc/Server/EndpointSettings.cs b/src/UiPath.CoreIpc/Server/EndpointSettings.cs index 3b00df73..65023197 100644 --- a/src/UiPath.CoreIpc/Server/EndpointSettings.cs +++ b/src/UiPath.CoreIpc/Server/EndpointSettings.cs @@ -5,7 +5,7 @@ public record EndpointSettings { public TaskScheduler? Scheduler { get; set; } - public BeforeCallHandler? BeforeIncommingCall { get; set; } + public BeforeCallHandler? BeforeIncomingCall { get; set; } public Type ContractType => Service.Type; public object? ServiceInstance => Service.MaybeGetInstance(); public IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider(); diff --git a/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs b/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs deleted file mode 100644 index bb68f927..00000000 --- a/src/UiPath.CoreIpc/Server/ServerTransportRunner.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace UiPath.Ipc; - -internal static class ServerTransportRunner -{ - public static async Task Start(ServerTransport transport) - { - var serverState = transport.CreateServerState(); - return serverState; - } -} diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs index d6bd0aff..0ccfc08e 100644 --- a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeClientTransport.cs @@ -11,9 +11,9 @@ public sealed record NamedPipeClientTransport : ClientTransport public override string ToString() => $"ClientPipe={PipeName}"; - public override IClientState CreateState() => new NamedPipeClientState(); + internal override IClientState CreateState() => new NamedPipeClientState(); - public override void Validate() + internal override void Validate() { if (PipeName is null or "") { diff --git a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs index aff79351..2319ae4c 100644 --- a/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs +++ b/src/UiPath.CoreIpc/Transport/NamedPipe/NamedPipeServerTransport.cs @@ -11,10 +11,10 @@ public sealed class NamedPipeServerTransport : ServerTransport [JsonIgnore] public AccessControlDelegate? AccessControl { get; init; } - protected internal override IServerState CreateServerState() + internal override IServerState CreateServerState() => new ServerState { Transport = this }; - protected override IEnumerable ValidateCore() + internal override IEnumerable ValidateCore() { yield return IsNotNull(PipeName); yield return IsNotNull(ServerName); @@ -69,7 +69,15 @@ NamedPipeServerStream CreateStream() public required NamedPipeServerStream Stream { get; init; } - void IDisposable.Dispose() => Stream.Dispose(); + ValueTask IAsyncDisposable.DisposeAsync() + { +#if NET461 + Stream.Dispose(); + return default; +#else + return Stream.DisposeAsync(); +#endif + } async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) { diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs index dd7cf5a3..f82dc7f0 100644 --- a/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpClientTransport.cs @@ -8,9 +8,9 @@ public sealed record TcpClientTransport : ClientTransport public override string ToString() => $"TcpClient={EndPoint}"; - public override IClientState CreateState() => new TcpClientState(); + internal override IClientState CreateState() => new TcpClientState(); - public override void Validate() + internal override void Validate() { if (EndPoint is null) { diff --git a/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs b/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs index 18bb0a5f..33c74f3d 100644 --- a/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs +++ b/src/UiPath.CoreIpc/Transport/Tcp/TcpServerTransport.cs @@ -7,14 +7,14 @@ public sealed class TcpServerTransport : ServerTransport { public required IPEndPoint EndPoint { get; init; } - protected internal override IServerState CreateServerState() + internal override IServerState CreateServerState() { var listener = new TcpListener(EndPoint); listener.Start(backlog: ConcurrentAccepts); return new ServerState() { TcpListener = listener }; } - protected override IEnumerable ValidateCore() + internal override IEnumerable ValidateCore() { yield return IsNotNull(EndPoint); } @@ -51,6 +51,6 @@ async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken return tcpClient.GetStream(); } - void IDisposable.Dispose() { } + ValueTask IAsyncDisposable.DisposeAsync() => default; } } diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs index e5b36472..6aa333f1 100644 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketClientTransport.cs @@ -7,9 +7,9 @@ public sealed record WebSocketClientTransport : ClientTransport public required Uri Uri { get; init; } public override string ToString() => $"WebSocketClient={Uri}"; - public override IClientState CreateState() => new WebSocketClientState(); + internal override IClientState CreateState() => new WebSocketClientState(); - public override void Validate() + internal override void Validate() { if (Uri is null) { diff --git a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs index 14110555..e81536c1 100644 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketServerTransport.cs @@ -1,25 +1,31 @@ namespace UiPath.Ipc.Transport.WebSocket; -public sealed class WebSocketServerTransport : ServerTransport, ServerTransport.IServerState, ServerTransport.IServerConnectionSlot +public sealed class WebSocketServerTransport : ServerTransport { public required Accept Accept { get; init; } - protected internal override IServerState CreateServerState() => this; + internal override IServerState CreateServerState() => new State { Transport = this }; - IServerConnectionSlot IServerState.CreateConnectionSlot() => this; - - async ValueTask IServerConnectionSlot.AwaitConnection(CancellationToken ct) - { - var webSocket = await Accept(ct); - return new WebSocketStream(webSocket); - } - ValueTask IAsyncDisposable.DisposeAsync() => default; - void IDisposable.Dispose() { } - - protected override IEnumerable ValidateCore() + 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/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index d2bb74a6..781bb27e 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -54,7 +54,7 @@ - + @@ -65,7 +65,6 @@ - - + \ 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 index a866328c..83beb956 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -1,8 +1,8 @@ [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.TV")] [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")] @@ -19,8 +19,6 @@ namespace UiPath.Ipc public abstract class ClientTransport : System.IEquatable { protected ClientTransport() { } - public abstract UiPath.Ipc.IClientState CreateState(); - public abstract void Validate(); } public class EndpointCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable { @@ -41,7 +39,7 @@ namespace UiPath.Ipc { public EndpointSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } public EndpointSettings(System.Type contractType, object? serviceInstance = null) { } - public System.Func? BeforeIncommingCall { get; set; } + public System.Func? BeforeIncomingCall { get; set; } public System.Type ContractType { get; } public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } public object? ServiceInstance { get; } @@ -74,12 +72,6 @@ namespace UiPath.Ipc where TCallbackInterface : class; void Impersonate(System.Action action); } - public interface IClientState : System.IDisposable - { - System.IO.Stream? Network { get; } - System.Threading.Tasks.ValueTask Connect(UiPath.Ipc.IpcClient client, System.Threading.CancellationToken ct); - bool IsConnected(); - } 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) { } @@ -90,7 +82,14 @@ namespace UiPath.Ipc public static System.IO.Pipes.PipeSecurity LocalOnly(this System.IO.Pipes.PipeSecurity pipeSecurity) { } public static bool PipeExists(string pipeName, int timeout = 1) { } } - public sealed class IpcClient : UiPath.Ipc.Peer + 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)] @@ -113,7 +112,7 @@ namespace UiPath.Ipc public void Dispose() { } protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) { } } - public sealed class IpcServer : UiPath.Ipc.Peer, System.IAsyncDisposable + 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)] @@ -144,13 +143,6 @@ namespace UiPath.Ipc public Message(TPayload payload) { } public TPayload Payload { get; } } - public abstract class Peer - { - protected Peer() { } - public System.TimeSpan RequestTimeout { get; set; } - public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } - public System.IServiceProvider? ServiceProvider { get; set; } - } [System.Serializable] public class RemoteException : System.Exception { @@ -167,18 +159,6 @@ namespace UiPath.Ipc protected ServerTransport() { } public int ConcurrentAccepts { get; set; } public byte MaxReceivedMessageSizeInMegabytes { get; set; } - public System.Security.Cryptography.X509Certificates.X509Certificate? Certificate { get; init; } - protected abstract UiPath.Ipc.ServerTransport.IServerState CreateServerState(); - protected abstract System.Collections.Generic.IEnumerable ValidateCore(); - protected static string? IsNotNull(T? propertyValue, [System.Runtime.CompilerServices.CallerArgumentExpression("propertyValue")] string? propertyName = null) { } - protected interface IServerConnectionSlot : System.IDisposable - { - System.Threading.Tasks.ValueTask AwaitConnection(System.Threading.CancellationToken ct); - } - protected interface IServerState : System.IAsyncDisposable - { - UiPath.Ipc.ServerTransport.IServerConnectionSlot CreateConnectionSlot(); - } } } namespace UiPath.Ipc.Transport.NamedPipe @@ -191,9 +171,7 @@ namespace UiPath.Ipc.Transport.NamedPipe public bool AllowImpersonation { get; init; } public string PipeName { get; init; } public string ServerName { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } - public override void Validate() { } } public sealed class NamedPipeServerTransport : UiPath.Ipc.ServerTransport { @@ -204,9 +182,7 @@ namespace UiPath.Ipc.Transport.NamedPipe public System.Action? AccessControl { get; init; } public string PipeName { get; init; } public string ServerName { get; init; } - protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } namespace UiPath.Ipc.Transport.Tcp @@ -217,9 +193,7 @@ namespace UiPath.Ipc.Transport.Tcp "your compiler.", true)] public TcpClientTransport() { } public System.Net.IPEndPoint EndPoint { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } - public override void Validate() { } } public sealed class TcpServerTransport : UiPath.Ipc.ServerTransport { @@ -227,9 +201,7 @@ namespace UiPath.Ipc.Transport.Tcp "your compiler.", true)] public TcpServerTransport() { } public System.Net.IPEndPoint EndPoint { get; init; } - protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } namespace UiPath.Ipc.Transport.WebSocket @@ -240,18 +212,14 @@ namespace UiPath.Ipc.Transport.WebSocket "your compiler.", true)] public WebSocketClientTransport() { } public System.Uri Uri { get; init; } - public override UiPath.Ipc.IClientState CreateState() { } public override string ToString() { } - public override void Validate() { } } - public sealed class WebSocketServerTransport : UiPath.Ipc.ServerTransport, System.IAsyncDisposable, System.IDisposable, UiPath.Ipc.ServerTransport.IServerConnectionSlot, UiPath.Ipc.ServerTransport.IServerState + 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; } - protected override UiPath.Ipc.ServerTransport.IServerState CreateServerState() { } public override string ToString() { } - protected override System.Collections.Generic.IEnumerable ValidateCore() { } } } \ No newline at end of file diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.Ipc.Tests/TestBase.cs index 5652726f..cd15246a 100644 --- a/src/UiPath.Ipc.Tests/TestBase.cs +++ b/src/UiPath.Ipc.Tests/TestBase.cs @@ -87,7 +87,7 @@ async Task Core() var endpointSettings = new EndpointSettings(ContractType) { - BeforeIncommingCall = (callInfo, ct) => + BeforeIncomingCall = (callInfo, ct) => { _serverBeforeCalls.Add(callInfo); return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask; diff --git a/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj b/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj index 3f326d6d..6b62feb9 100644 --- a/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj +++ b/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj @@ -24,7 +24,7 @@ - + From 67f1445567cda5f793b6028368de81d1712fa4ad Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 26 Nov 2024 15:37:31 +0100 Subject: [PATCH 07/26] taking shape --- src/Playground/Program.cs | 3 ++- src/UiPath.CoreIpc/Config/IpcClient.cs | 8 +++----- src/UiPath.CoreIpc/Wire/Dtos.cs | 4 ++-- src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs | 1 - 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index fbfeed13..26577244 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -88,7 +88,8 @@ private static async Task Main(string[] args) PipeName = Contracts.PipeName, ServerName = ".", AllowImpersonation = false, - } + }, + DebugName = "Client1", }; var c2 = new IpcClient() diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index 1b95fd3a..942c5739 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -1,6 +1,4 @@ -using System.ComponentModel; - -namespace UiPath.Ipc; +namespace UiPath.Ipc; public sealed class IpcClient : IpcBase, IClientConfig { @@ -10,8 +8,7 @@ public sealed class IpcClient : IpcBase, IClientConfig public BeforeConnectHandler? BeforeConnect { get; set; } public BeforeCallHandler? BeforeOutgoingCall { get; set; } - [EditorBrowsable(EditorBrowsableState.Never)] - public string DebugName { get; set; } = null!; + internal string DebugName { get; set; } = null!; public required ClientTransport Transport { get; init; } @@ -26,6 +23,7 @@ private ServiceClient GetServiceClient(Type proxyType) } public TProxy GetProxy() where TProxy : class => GetServiceClient(typeof(TProxy)).GetProxy(); + // TODO: should decommission? internal void Validate() { var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; diff --git a/src/UiPath.CoreIpc/Wire/Dtos.cs b/src/UiPath.CoreIpc/Wire/Dtos.cs index 7dfcba58..6f796e0c 100644 --- a/src/UiPath.CoreIpc/Wire/Dtos.cs +++ b/src/UiPath.CoreIpc/Wire/Dtos.cs @@ -48,7 +48,7 @@ public TResult Deserialize() return (TResult)(DownloadStream ?? IpcJsonSerializer.Instance.Deserialize(Data ?? "", typeof(TResult)))!; } } -[Serializable] + public record Error(string Message, string StackTrace, string Type, Error? InnerError) { [return: NotNullIfNotNull("exception")] @@ -64,7 +64,7 @@ public record Error(string Message, string StackTrace, string Type, Error? Inner 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)) diff --git a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs index 037e9fc1..92604922 100644 --- a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs +++ b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs @@ -1,6 +1,5 @@ namespace UiPath.Ipc; -[Serializable] public sealed class EndpointNotFoundException : ArgumentOutOfRangeException { public string ServerDebugName { get; } From cd06d859c9cb30aa1345d5dca066ec9ccd731ac6 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 26 Nov 2024 15:44:56 +0100 Subject: [PATCH 08/26] taking shape --- src/UiPath.CoreIpc/Helpers/Helpers.cs | 3 +++ src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/UiPath.CoreIpc/Helpers/Helpers.cs b/src/UiPath.CoreIpc/Helpers/Helpers.cs index abbfedf9..926cdb00 100644 --- a/src/UiPath.CoreIpc/Helpers/Helpers.cs +++ b/src/UiPath.CoreIpc/Helpers/Helpers.cs @@ -1,5 +1,6 @@ using Microsoft.IO; using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO.Pipes; using System.Runtime.InteropServices; using System.Security.AccessControl; @@ -136,6 +137,8 @@ public static PipeSecurity AllowCurrentUser(this PipeSecurity pipeSecurity, bool return pipeSecurity; } + [Browsable(false)] + [EditorBrowsable( EditorBrowsableState.Never)] public static bool PipeExists(string pipeName, int timeout = 1) { try diff --git a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs index 92604922..de6ba869 100644 --- a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs +++ b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs @@ -1,11 +1,11 @@ namespace UiPath.Ipc; -public sealed class EndpointNotFoundException : ArgumentOutOfRangeException +public sealed class EndpointNotFoundException : ArgumentException { public string ServerDebugName { get; } public string EndpointName { get; } - public EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) + internal EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) : base(paramName, FormatMessage(serverDebugName, endpointName)) { ServerDebugName = serverDebugName; From ce1a19b241972754e8ffeccaf7904019a663d0ac Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 26 Nov 2024 16:00:44 +0100 Subject: [PATCH 09/26] taking shape --- .../report/UiPath.Ipc.net6.0-windows.received.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 index 83beb956..8cfda51b 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -28,10 +28,8 @@ namespace UiPath.Ipc public void Add(System.Type contractType, object? instance) { } public System.Collections.Generic.IEnumerator GetEnumerator() { } } - [System.Serializable] - public sealed class EndpointNotFoundException : System.ArgumentOutOfRangeException + public sealed class EndpointNotFoundException : System.ArgumentException { - public EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) { } public string EndpointName { get; } public string ServerDebugName { get; } } @@ -54,7 +52,6 @@ namespace UiPath.Ipc public EndpointSettings(TContract? serviceInstance = null) { } public override UiPath.Ipc.EndpointSettings WithServiceProvider(System.IServiceProvider? serviceProvider) { } } - [System.Serializable] public class Error : System.IEquatable { public Error(string Message, string StackTrace, string Type, UiPath.Ipc.Error? InnerError) { } @@ -80,6 +77,7 @@ namespace UiPath.Ipc 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 @@ -97,7 +95,6 @@ namespace UiPath.Ipc public System.Func? BeforeConnect { get; set; } public System.Func? BeforeOutgoingCall { get; set; } public UiPath.Ipc.EndpointCollection? Callbacks { get; set; } - public string DebugName { get; set; } public Microsoft.Extensions.Logging.ILogger? Logger { get; init; } public UiPath.Ipc.ClientTransport Transport { get; init; } public TProxy GetProxy() @@ -143,7 +140,6 @@ namespace UiPath.Ipc public Message(TPayload payload) { } public TPayload Payload { get; } } - [System.Serializable] public class RemoteException : System.Exception { public RemoteException(UiPath.Ipc.Error error) { } From 1e3211a59781c3e99cd29db27bfc93ea811f9f14 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 27 Nov 2024 12:40:21 +0100 Subject: [PATCH 10/26] taking shape --- src/UiPath.CoreIpc/Config/EndpointCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UiPath.CoreIpc/Config/EndpointCollection.cs b/src/UiPath.CoreIpc/Config/EndpointCollection.cs index 8174056f..ac13caa0 100644 --- a/src/UiPath.CoreIpc/Config/EndpointCollection.cs +++ b/src/UiPath.CoreIpc/Config/EndpointCollection.cs @@ -2,7 +2,7 @@ namespace UiPath.Ipc; -public class EndpointCollection : IEnumerable, IEnumerable +public class EndpointCollection : IEnumerable { internal readonly Dictionary Endpoints = new(); From 0d7f45d3068328ecb02a4c776599ef5821469377 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 27 Nov 2024 22:46:01 +0100 Subject: [PATCH 11/26] rename Endpoint.. to Contract.. - simplifications --- src/Playground/Program.cs | 2 +- src/UiPath.CoreIpc/Client/ServiceClient.cs | 1 + src/UiPath.CoreIpc/Config/ClientConfig.cs | 16 -- src/UiPath.CoreIpc/Config/ClientTransport.cs | 3 + ...intCollection.cs => ContractCollection.cs} | 13 +- src/UiPath.CoreIpc/Config/IpcClient.cs | 28 +-- src/UiPath.CoreIpc/Config/IpcServer.cs | 8 +- src/UiPath.CoreIpc/Config/ServerTransport.cs | 2 + src/UiPath.CoreIpc/GlobalUsings.cs | 2 +- .../Helpers/DefaultsExtensions.cs | 2 +- src/UiPath.CoreIpc/Helpers/Router.cs | 6 +- src/UiPath.CoreIpc/PublicAPI.Shipped.txt | 177 ------------------ src/UiPath.CoreIpc/PublicAPI.Unshipped.txt | 1 - src/UiPath.CoreIpc/Server/ContractSettings.cs | 44 +++++ src/UiPath.CoreIpc/Server/EndpointSettings.cs | 58 ------ src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 8 - .../UiPath.Ipc.net6.0-windows.received.txt | 44 ++--- src/UiPath.Ipc.Tests/ComputingTests.cs | 2 +- src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs | 2 +- src/UiPath.Ipc.Tests/RobotTests.cs | 2 +- .../RobotTestsOverNamedPipes.cs | 16 +- src/UiPath.Ipc.Tests/TestBase.cs | 6 +- 22 files changed, 101 insertions(+), 342 deletions(-) delete mode 100644 src/UiPath.CoreIpc/Config/ClientConfig.cs rename src/UiPath.CoreIpc/Config/{EndpointCollection.cs => ContractCollection.cs} (50%) delete mode 100644 src/UiPath.CoreIpc/PublicAPI.Shipped.txt delete mode 100644 src/UiPath.CoreIpc/PublicAPI.Unshipped.txt create mode 100644 src/UiPath.CoreIpc/Server/ContractSettings.cs delete mode 100644 src/UiPath.CoreIpc/Server/EndpointSettings.cs diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index 26577244..45d6453a 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -42,7 +42,7 @@ private static async Task Main(string[] args) Endpoints = new() { typeof(Contracts.IServerOperations), // DEVINE - new EndpointSettings(typeof(Contracts.IServerOperations)) // ASTALALT + new ContractSettings(typeof(Contracts.IServerOperations)) // ASTALALT { BeforeIncomingCall = async (callInfo, _) => { diff --git a/src/UiPath.CoreIpc/Client/ServiceClient.cs b/src/UiPath.CoreIpc/Client/ServiceClient.cs index 95ef34c3..bdde0ae7 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -183,6 +183,7 @@ private Connection? LatestConnection public ServiceClientProper(IpcClient client, Type interfaceType) : base(interfaceType) { _client = client; + client.Transport.Validate(); _clientState = client.Transport.CreateState(); } diff --git a/src/UiPath.CoreIpc/Config/ClientConfig.cs b/src/UiPath.CoreIpc/Config/ClientConfig.cs deleted file mode 100644 index 6c05c192..00000000 --- a/src/UiPath.CoreIpc/Config/ClientConfig.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace UiPath.Ipc; - -public sealed class ClientConfig : Peer, IServiceClientConfig -{ - public BeforeCallHandler? BeforeCall { get; init; } - - internal void Validate() - { - var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; - - if (haveDeferredInjectedCallbacks && ServiceProvider is null) - { - throw new InvalidOperationException("ServiceProvider is required when you register injectable callbacks. Consider registering a callback instance."); - } - } -} diff --git a/src/UiPath.CoreIpc/Config/ClientTransport.cs b/src/UiPath.CoreIpc/Config/ClientTransport.cs index 221c16e3..1ef3942d 100644 --- a/src/UiPath.CoreIpc/Config/ClientTransport.cs +++ b/src/UiPath.CoreIpc/Config/ClientTransport.cs @@ -2,6 +2,9 @@ public abstract record ClientTransport { + private protected ClientTransport() { } + internal abstract IClientState CreateState(); + internal abstract void Validate(); } diff --git a/src/UiPath.CoreIpc/Config/EndpointCollection.cs b/src/UiPath.CoreIpc/Config/ContractCollection.cs similarity index 50% rename from src/UiPath.CoreIpc/Config/EndpointCollection.cs rename to src/UiPath.CoreIpc/Config/ContractCollection.cs index ac13caa0..0e1b3252 100644 --- a/src/UiPath.CoreIpc/Config/EndpointCollection.cs +++ b/src/UiPath.CoreIpc/Config/ContractCollection.cs @@ -2,20 +2,19 @@ namespace UiPath.Ipc; -public class EndpointCollection : IEnumerable +public class ContractCollection : IEnumerable { - internal readonly Dictionary Endpoints = new(); + internal readonly Dictionary Endpoints = new(); - public void Add(Type type) => Add(type, instance: null); - public void Add(Type contractType, object? instance) => Add(new EndpointSettings(contractType, instance)); - public void Add(EndpointSettings endpointSettings) + 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)); - // endpointSettings.Validate(); Endpoints[endpointSettings.Service.Type] = endpointSettings; } - public IEnumerator GetEnumerator() => Endpoints.Values.GetEnumerator(); + public IEnumerator GetEnumerator() => Endpoints.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index 942c5739..79ae4220 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -2,7 +2,7 @@ public sealed class IpcClient : IpcBase, IClientConfig { - public EndpointCollection? Callbacks { get; set; } + public ContractCollection? Callbacks { get; set; } public ILogger? Logger { get; init; } public BeforeConnectHandler? BeforeConnect { get; set; } @@ -23,24 +23,6 @@ private ServiceClient GetServiceClient(Type proxyType) } public TProxy GetProxy() where TProxy : class => GetServiceClient(typeof(TProxy)).GetProxy(); - // TODO: should decommission? - internal void Validate() - { - var haveDeferredInjectedCallbacks = Callbacks?.Any(x => x.Service.MaybeGetServiceProvider() is null && x.Service.MaybeGetInstance() is null) ?? false; - - if (haveDeferredInjectedCallbacks && ServiceProvider is null) - { - throw new InvalidOperationException("ServiceProvider is required when you register injectable callbacks. Consider registering a callback instance."); - } - - if (Transport is null) - { - throw new InvalidOperationException($"{Transport} is required."); - } - - Transport.Validate(); - } - internal ILogger? GetLogger(string name) { if (Logger is not null) @@ -59,9 +41,11 @@ internal void Validate() internal RouterConfig CreateCallbackRouterConfig() => RouterConfig.From( Callbacks.OrDefault(), - endpoint => endpoint with + endpoint => { - BeforeIncomingCall = null, // callbacks don't support BeforeCall - Scheduler = endpoint.Scheduler ?? Scheduler + 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 index da5083db..0fbc3d40 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -4,7 +4,7 @@ namespace UiPath.Ipc; public sealed class IpcServer : IpcBase, IAsyncDisposable { - public required EndpointCollection Endpoints { get; init; } + public required ContractCollection Endpoints { get; init; } public required ServerTransport Transport { get; init; } private readonly object _lock = new(); @@ -87,9 +87,11 @@ private void OnNewConnectionError(Exception ex) internal RouterConfig CreateRouterConfig(IpcServer server) => RouterConfig.From( server.Endpoints, - endpoint => endpoint with + endpoint => { - Scheduler = endpoint.Scheduler ?? server.Scheduler + var clone = new ContractSettings(endpoint); + clone.Scheduler ??= server.Scheduler; + return clone; }); private sealed class ObserverAdapter : IObserver diff --git a/src/UiPath.CoreIpc/Config/ServerTransport.cs b/src/UiPath.CoreIpc/Config/ServerTransport.cs index 1d0c27c8..8c66a7e7 100644 --- a/src/UiPath.CoreIpc/Config/ServerTransport.cs +++ b/src/UiPath.CoreIpc/Config/ServerTransport.cs @@ -5,6 +5,8 @@ namespace UiPath.Ipc; public abstract class ServerTransport { + private protected ServerTransport() { } + public int ConcurrentAccepts { get; set; } = 5; public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; diff --git a/src/UiPath.CoreIpc/GlobalUsings.cs b/src/UiPath.CoreIpc/GlobalUsings.cs index 00369edd..4fd94c5b 100644 --- a/src/UiPath.CoreIpc/GlobalUsings.cs +++ b/src/UiPath.CoreIpc/GlobalUsings.cs @@ -2,5 +2,5 @@ global using BeforeCallHandler = System.Func; global using InvokeDelegate = System.Func; global using Accept = System.Func>; -global using ContractToSettingsMap = System.Collections.Generic.Dictionary; +global using ContractToSettingsMap = System.Collections.Generic.Dictionary; global using AccessControlDelegate = System.Action; diff --git a/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs index 4b6d9ca1..82ebf122 100644 --- a/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs +++ b/src/UiPath.CoreIpc/Helpers/DefaultsExtensions.cs @@ -10,7 +10,7 @@ internal static class DefaultsExtensions 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 EndpointCollection OrDefault(this EndpointCollection? endpoints) => endpoints ?? new(); + public static ContractCollection OrDefault(this ContractCollection? endpoints) => endpoints ?? new(); public static Func? MaybeCreateServiceFactory(this IServiceProvider? serviceProvider) where T : class { diff --git a/src/UiPath.CoreIpc/Helpers/Router.cs b/src/UiPath.CoreIpc/Helpers/Router.cs index 7906f013..9a023c46 100644 --- a/src/UiPath.CoreIpc/Helpers/Router.cs +++ b/src/UiPath.CoreIpc/Helpers/Router.cs @@ -1,8 +1,8 @@ namespace UiPath.Ipc; -internal readonly record struct RouterConfig(IReadOnlyDictionary Endpoints) +internal readonly record struct RouterConfig(IReadOnlyDictionary Endpoints) { - public static RouterConfig From(EndpointCollection endpoints, Func transform) + public static RouterConfig From(ContractCollection endpoints, Func transform) { ContractToSettingsMap nameToEndpoint = []; @@ -127,7 +127,7 @@ public override ServiceFactory WithProvider(IServiceProvider? serviceProvider) internal readonly struct Route { - public static Route From(IServiceProvider? serviceProvider, EndpointSettings endpointSettings) + public static Route From(IServiceProvider? serviceProvider, ContractSettings endpointSettings) => new Route() { Service = endpointSettings.Service.WithProvider(serviceProvider), diff --git a/src/UiPath.CoreIpc/PublicAPI.Shipped.txt b/src/UiPath.CoreIpc/PublicAPI.Shipped.txt deleted file mode 100644 index 3a9b8f82..00000000 --- a/src/UiPath.CoreIpc/PublicAPI.Shipped.txt +++ /dev/null @@ -1,177 +0,0 @@ -#nullable enable -abstract UiPath.Ipc.ClientTransport.CreateState() -> UiPath.Ipc.IClientState! -abstract UiPath.Ipc.ClientTransport.Validate() -> void -abstract UiPath.Ipc.ServerTransport.CreateServerState() -> UiPath.Ipc.ServerTransport.IServerState! -abstract UiPath.Ipc.ServerTransport.ValidateCore() -> System.Collections.Generic.IEnumerable! -override UiPath.Ipc.EndpointSettings.WithServiceProvider(System.IServiceProvider? serviceProvider) -> UiPath.Ipc.EndpointSettings! -override UiPath.Ipc.Error.ToString() -> string! -override UiPath.Ipc.IpcProxy.Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) -> object? -override UiPath.Ipc.RemoteException.StackTrace.get -> string! -override UiPath.Ipc.RemoteException.ToString() -> string! -override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.CreateState() -> UiPath.Ipc.IClientState! -override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ToString() -> string! -override UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.Validate() -> void -override UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ToString() -> string! -override UiPath.Ipc.Transport.Tcp.TcpClientTransport.CreateState() -> UiPath.Ipc.IClientState! -override UiPath.Ipc.Transport.Tcp.TcpClientTransport.ToString() -> string! -override UiPath.Ipc.Transport.Tcp.TcpClientTransport.Validate() -> void -override UiPath.Ipc.Transport.Tcp.TcpServerTransport.ToString() -> string! -override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.CreateState() -> UiPath.Ipc.IClientState! -override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.ToString() -> string! -override UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Validate() -> void -override UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.ToString() -> string! -static UiPath.Ipc.Error.FromException(System.Exception? exception) -> UiPath.Ipc.Error? -static UiPath.Ipc.IOHelpers.Allow(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.IdentityReference! sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.Allow(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.AllowCurrentUser(this System.IO.Pipes.PipeSecurity! pipeSecurity, bool onlyNonAdmin = false) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.Deny(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.IdentityReference! sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.Deny(this System.IO.Pipes.PipeSecurity! pipeSecurity, System.Security.Principal.WellKnownSidType sid, System.IO.Pipes.PipeAccessRights pipeAccessRights) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.LocalOnly(this System.IO.Pipes.PipeSecurity! pipeSecurity) -> System.IO.Pipes.PipeSecurity! -static UiPath.Ipc.IOHelpers.PipeExists(string! pipeName, int timeout = 1) -> bool -static UiPath.Ipc.ServerTransport.IsNotNull(T? propertyValue, string? propertyName = null) -> string? -UiPath.Ipc.CallInfo -UiPath.Ipc.CallInfo.Arguments.get -> object?[]! -UiPath.Ipc.CallInfo.CallInfo() -> void -UiPath.Ipc.CallInfo.CallInfo(bool newConnection, System.Reflection.MethodInfo! method, object?[]! arguments) -> void -UiPath.Ipc.CallInfo.Method.get -> System.Reflection.MethodInfo! -UiPath.Ipc.CallInfo.NewConnection.get -> bool -UiPath.Ipc.ClientTransport -UiPath.Ipc.EndpointCollection -UiPath.Ipc.EndpointCollection.Add(System.Type! contractType, object? instance) -> void -UiPath.Ipc.EndpointCollection.Add(System.Type! type) -> void -UiPath.Ipc.EndpointCollection.Add(UiPath.Ipc.EndpointSettings! endpointSettings) -> void -UiPath.Ipc.EndpointCollection.EndpointCollection() -> void -UiPath.Ipc.EndpointCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -UiPath.Ipc.EndpointNotFoundException -UiPath.Ipc.EndpointNotFoundException.EndpointName.get -> string! -UiPath.Ipc.EndpointNotFoundException.EndpointNotFoundException(string! paramName, string! serverDebugName, string! endpointName) -> void -UiPath.Ipc.EndpointNotFoundException.ServerDebugName.get -> string! -UiPath.Ipc.EndpointSettings -UiPath.Ipc.EndpointSettings.BeforeIncommingCall.get -> System.Func? -UiPath.Ipc.EndpointSettings.BeforeIncommingCall.set -> void -UiPath.Ipc.EndpointSettings.ContractType.get -> System.Type! -UiPath.Ipc.EndpointSettings.EndpointSettings(System.Type! contractType, object? serviceInstance = null) -> void -UiPath.Ipc.EndpointSettings.EndpointSettings(System.Type! contractType, System.IServiceProvider! serviceProvider) -> void -UiPath.Ipc.EndpointSettings.Scheduler.get -> System.Threading.Tasks.TaskScheduler? -UiPath.Ipc.EndpointSettings.Scheduler.set -> void -UiPath.Ipc.EndpointSettings.ServiceInstance.get -> object? -UiPath.Ipc.EndpointSettings.ServiceProvider.get -> System.IServiceProvider? -UiPath.Ipc.EndpointSettings.Validate() -> void -UiPath.Ipc.EndpointSettings -UiPath.Ipc.EndpointSettings.EndpointSettings(System.IServiceProvider! serviceProvider) -> void -UiPath.Ipc.EndpointSettings.EndpointSettings(TContract? serviceInstance = null) -> void -UiPath.Ipc.Error -UiPath.Ipc.Error.Error(string! Message, string! StackTrace, string! Type, UiPath.Ipc.Error? InnerError) -> void -UiPath.Ipc.Error.InnerError.get -> UiPath.Ipc.Error? -UiPath.Ipc.Error.InnerError.init -> void -UiPath.Ipc.Error.Message.get -> string! -UiPath.Ipc.Error.Message.init -> void -UiPath.Ipc.Error.StackTrace.get -> string! -UiPath.Ipc.Error.StackTrace.init -> void -UiPath.Ipc.Error.Type.get -> string! -UiPath.Ipc.Error.Type.init -> void -UiPath.Ipc.IClient -UiPath.Ipc.IClient.GetCallback() -> TCallbackInterface! -UiPath.Ipc.IClient.Impersonate(System.Action! action) -> void -UiPath.Ipc.IClientState -UiPath.Ipc.IClientState.Connect(UiPath.Ipc.IpcClient! client, System.Threading.CancellationToken ct) -> System.Threading.Tasks.ValueTask -UiPath.Ipc.IClientState.IsConnected() -> bool -UiPath.Ipc.IClientState.Network.get -> System.IO.Stream? -UiPath.Ipc.IOHelpers -UiPath.Ipc.IpcClient -UiPath.Ipc.IpcClient.BeforeConnect.get -> System.Func? -UiPath.Ipc.IpcClient.BeforeConnect.set -> void -UiPath.Ipc.IpcClient.BeforeOutgoingCall.get -> System.Func? -UiPath.Ipc.IpcClient.BeforeOutgoingCall.set -> void -UiPath.Ipc.IpcClient.Callbacks.get -> UiPath.Ipc.EndpointCollection? -UiPath.Ipc.IpcClient.Callbacks.set -> void -UiPath.Ipc.IpcClient.DebugName.get -> string! -UiPath.Ipc.IpcClient.DebugName.set -> void -UiPath.Ipc.IpcClient.GetProxy() -> TProxy! -UiPath.Ipc.IpcClient.IpcClient() -> void -UiPath.Ipc.IpcClient.Logger.get -> Microsoft.Extensions.Logging.ILogger? -UiPath.Ipc.IpcClient.Logger.init -> void -UiPath.Ipc.IpcClient.Transport.get -> UiPath.Ipc.ClientTransport! -UiPath.Ipc.IpcClient.Transport.init -> void -UiPath.Ipc.IpcProxy -UiPath.Ipc.IpcProxy.CloseConnection() -> System.Threading.Tasks.ValueTask -UiPath.Ipc.IpcProxy.ConnectionClosed -> System.EventHandler! -UiPath.Ipc.IpcProxy.Dispose() -> void -UiPath.Ipc.IpcProxy.IpcProxy() -> void -UiPath.Ipc.IpcProxy.Network.get -> System.IO.Stream? -UiPath.Ipc.IpcServer -UiPath.Ipc.IpcServer.DisposeAsync() -> System.Threading.Tasks.ValueTask -UiPath.Ipc.IpcServer.Endpoints.get -> UiPath.Ipc.EndpointCollection! -UiPath.Ipc.IpcServer.Endpoints.init -> void -UiPath.Ipc.IpcServer.IpcServer() -> void -UiPath.Ipc.IpcServer.Start() -> void -UiPath.Ipc.IpcServer.Transport.get -> UiPath.Ipc.ServerTransport! -UiPath.Ipc.IpcServer.Transport.init -> void -UiPath.Ipc.IpcServer.WaitForStart() -> System.Threading.Tasks.Task! -UiPath.Ipc.IpcServer.WaitForStop() -> System.Threading.Tasks.Task! -UiPath.Ipc.Message -UiPath.Ipc.Message.Client.get -> UiPath.Ipc.IClient! -UiPath.Ipc.Message.Client.set -> void -UiPath.Ipc.Message.GetCallback() -> TCallbackInterface! -UiPath.Ipc.Message.ImpersonateClient(System.Action! action) -> void -UiPath.Ipc.Message.Message() -> void -UiPath.Ipc.Message.RequestTimeout.get -> System.TimeSpan -UiPath.Ipc.Message.RequestTimeout.set -> void -UiPath.Ipc.Message -UiPath.Ipc.Message.Message(TPayload payload) -> void -UiPath.Ipc.Message.Payload.get -> TPayload -UiPath.Ipc.Peer -UiPath.Ipc.Peer.Peer() -> void -UiPath.Ipc.Peer.RequestTimeout.get -> System.TimeSpan -UiPath.Ipc.Peer.RequestTimeout.set -> void -UiPath.Ipc.Peer.Scheduler.get -> System.Threading.Tasks.TaskScheduler? -UiPath.Ipc.Peer.Scheduler.set -> void -UiPath.Ipc.Peer.ServiceProvider.get -> System.IServiceProvider? -UiPath.Ipc.Peer.ServiceProvider.set -> void -UiPath.Ipc.RemoteException -UiPath.Ipc.RemoteException.InnerException.get -> UiPath.Ipc.RemoteException? -UiPath.Ipc.RemoteException.Is() -> bool -UiPath.Ipc.RemoteException.RemoteException(UiPath.Ipc.Error! error) -> void -UiPath.Ipc.RemoteException.Type.get -> string! -UiPath.Ipc.ServerTransport -UiPath.Ipc.ServerTransport.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate? -UiPath.Ipc.ServerTransport.Certificate.init -> void -UiPath.Ipc.ServerTransport.ConcurrentAccepts.get -> int -UiPath.Ipc.ServerTransport.ConcurrentAccepts.set -> void -UiPath.Ipc.ServerTransport.IServerConnectionSlot -UiPath.Ipc.ServerTransport.IServerConnectionSlot.AwaitConnection(System.Threading.CancellationToken ct) -> System.Threading.Tasks.ValueTask -UiPath.Ipc.ServerTransport.IServerState -UiPath.Ipc.ServerTransport.IServerState.CreateConnectionSlot() -> UiPath.Ipc.ServerTransport.IServerConnectionSlot! -UiPath.Ipc.ServerTransport.MaxReceivedMessageSizeInMegabytes.get -> byte -UiPath.Ipc.ServerTransport.MaxReceivedMessageSizeInMegabytes.set -> void -UiPath.Ipc.ServerTransport.ServerTransport() -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.AllowImpersonation.get -> bool -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.AllowImpersonation.init -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.PipeName.get -> string! -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.PipeName.init -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ServerName.get -> string! -UiPath.Ipc.Transport.NamedPipe.NamedPipeClientTransport.ServerName.init -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.AccessControl.get -> System.Action? -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.AccessControl.init -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.NamedPipeServerTransport() -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.PipeName.get -> string! -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.PipeName.init -> void -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ServerName.get -> string! -UiPath.Ipc.Transport.NamedPipe.NamedPipeServerTransport.ServerName.init -> void -UiPath.Ipc.Transport.Tcp.TcpClientTransport -UiPath.Ipc.Transport.Tcp.TcpClientTransport.EndPoint.get -> System.Net.IPEndPoint! -UiPath.Ipc.Transport.Tcp.TcpClientTransport.EndPoint.init -> void -UiPath.Ipc.Transport.Tcp.TcpServerTransport -UiPath.Ipc.Transport.Tcp.TcpServerTransport.EndPoint.get -> System.Net.IPEndPoint! -UiPath.Ipc.Transport.Tcp.TcpServerTransport.EndPoint.init -> void -UiPath.Ipc.Transport.Tcp.TcpServerTransport.TcpServerTransport() -> void -UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport -UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Uri.get -> System.Uri! -UiPath.Ipc.Transport.WebSocket.WebSocketClientTransport.Uri.init -> void -UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport -UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.Accept.get -> System.Func!>! -UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.Accept.init -> void -UiPath.Ipc.Transport.WebSocket.WebSocketServerTransport.WebSocketServerTransport() -> void -virtual UiPath.Ipc.EndpointSettings.WithServiceProvider(System.IServiceProvider? serviceProvider) -> UiPath.Ipc.EndpointSettings! \ No newline at end of file diff --git a/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt b/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt deleted file mode 100644 index 5f282702..00000000 --- a/src/UiPath.CoreIpc/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ - \ 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/EndpointSettings.cs b/src/UiPath.CoreIpc/Server/EndpointSettings.cs deleted file mode 100644 index 65023197..00000000 --- a/src/UiPath.CoreIpc/Server/EndpointSettings.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace UiPath.Ipc; - -using System; - -public record EndpointSettings -{ - public TaskScheduler? Scheduler { get; set; } - public BeforeCallHandler? BeforeIncomingCall { get; set; } - public Type ContractType => Service.Type; - public object? ServiceInstance => Service.MaybeGetInstance(); - public IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider(); - internal ServiceFactory Service { get; } - - public EndpointSettings(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 EndpointSettings(Type contractType, IServiceProvider serviceProvider) : this( - new ServiceFactory.Injected() - { - Type = contractType ?? throw new ArgumentNullException(nameof(contractType)), - ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)) - }) - { } - - private protected EndpointSettings(ServiceFactory service) => Service = service; - - public virtual EndpointSettings WithServiceProvider(IServiceProvider? serviceProvider) - => new(Service.WithProvider(serviceProvider)); - - public void Validate() - { - Validator.Validate(Service.Type); - if (Service.MaybeGetInstance() is { } instance && !Service.Type.IsAssignableFrom(instance.GetType())) - { - throw new ArgumentOutOfRangeException(nameof(instance)); - } - } -} - -public sealed record EndpointSettings : EndpointSettings where TContract : class -{ - public EndpointSettings(TContract? serviceInstance = null) : base(typeof(TContract), serviceInstance) { } - public EndpointSettings(IServiceProvider serviceProvider) : base(typeof(TContract), serviceProvider) { } - private EndpointSettings(ServiceFactory service) : base(service) { } - - public override EndpointSettings WithServiceProvider(IServiceProvider? serviceProvider) - => new EndpointSettings(Service.WithProvider(serviceProvider)); -} diff --git a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index 781bb27e..85014d33 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -27,13 +27,6 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -42,7 +35,6 @@ - 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 index 8cfda51b..95fd1bdb 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -16,41 +16,26 @@ namespace UiPath.Ipc public System.Reflection.MethodInfo Method { get; } public bool NewConnection { get; } } - public abstract class ClientTransport : System.IEquatable + public abstract class ClientTransport : System.IEquatable { } + public class ContractCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable { - protected ClientTransport() { } - } - public class EndpointCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable - { - public EndpointCollection() { } - public void Add(System.Type type) { } - public void Add(UiPath.Ipc.EndpointSettings endpointSettings) { } + 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 EndpointNotFoundException : System.ArgumentException - { - public string EndpointName { get; } - public string ServerDebugName { get; } + public System.Collections.Generic.IEnumerator GetEnumerator() { } } - public class EndpointSettings : System.IEquatable + public sealed class ContractSettings { - public EndpointSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } - public EndpointSettings(System.Type contractType, object? serviceInstance = null) { } + public ContractSettings(System.Type contractType, System.IServiceProvider serviceProvider) { } + public ContractSettings(System.Type contractType, object? serviceInstance = null) { } public System.Func? BeforeIncomingCall { get; set; } - public System.Type ContractType { get; } public System.Threading.Tasks.TaskScheduler? Scheduler { get; set; } - public object? ServiceInstance { get; } - public System.IServiceProvider? ServiceProvider { get; } - public void Validate() { } - public virtual UiPath.Ipc.EndpointSettings WithServiceProvider(System.IServiceProvider? serviceProvider) { } } - public sealed class EndpointSettings : UiPath.Ipc.EndpointSettings, System.IEquatable> - where TContract : class + public sealed class EndpointNotFoundException : System.ArgumentException { - public EndpointSettings(System.IServiceProvider serviceProvider) { } - public EndpointSettings(TContract? serviceInstance = null) { } - public override UiPath.Ipc.EndpointSettings WithServiceProvider(System.IServiceProvider? serviceProvider) { } + public string EndpointName { get; } + public string ServerDebugName { get; } } public class Error : System.IEquatable { @@ -94,7 +79,7 @@ namespace UiPath.Ipc public IpcClient() { } public System.Func? BeforeConnect { get; set; } public System.Func? BeforeOutgoingCall { get; set; } - public UiPath.Ipc.EndpointCollection? Callbacks { 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() @@ -114,7 +99,7 @@ namespace UiPath.Ipc [System.Obsolete("Constructors of types with required members are not supported in this version of " + "your compiler.", true)] public IpcServer() { } - public UiPath.Ipc.EndpointCollection Endpoints { get; init; } + public UiPath.Ipc.ContractCollection Endpoints { get; init; } public UiPath.Ipc.ServerTransport Transport { get; init; } public System.Threading.Tasks.ValueTask DisposeAsync() { } [System.Diagnostics.CodeAnalysis.MemberNotNull(new string[] { @@ -152,7 +137,6 @@ namespace UiPath.Ipc } public abstract class ServerTransport { - protected ServerTransport() { } public int ConcurrentAccepts { get; set; } public byte MaxReceivedMessageSizeInMegabytes { get; set; } } diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.Ipc.Tests/ComputingTests.cs index aeecd313..2d7b1d86 100644 --- a/src/UiPath.Ipc.Tests/ComputingTests.cs +++ b/src/UiPath.Ipc.Tests/ComputingTests.cs @@ -24,7 +24,7 @@ public abstract class ComputingTests : SpyTestBase protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; protected sealed override Type ContractType => typeof(IComputingService); - protected override EndpointCollection? Callbacks => new() + protected override ContractCollection? Callbacks => new() { { typeof(IComputingCallback), _computingCallback } }; diff --git a/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs index 3ccc33c9..1f0c1217 100644 --- a/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs +++ b/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs @@ -47,7 +47,7 @@ public static IpcServer WithRequestTimeout(this IpcServer ipcServer, TimeSpan re public static async Task WithRequestTimeout(this Task ipcServerTask, TimeSpan requestTimeout) => (await ipcServerTask).WithRequestTimeout(requestTimeout); - public static IpcClient WithCallbacks(this IpcClient ipcClient, EndpointCollection callbacks) + public static IpcClient WithCallbacks(this IpcClient ipcClient, ContractCollection callbacks) { ipcClient.Callbacks = callbacks; return ipcClient; diff --git a/src/UiPath.Ipc.Tests/RobotTests.cs b/src/UiPath.Ipc.Tests/RobotTests.cs index 627bb566..bc173e51 100644 --- a/src/UiPath.Ipc.Tests/RobotTests.cs +++ b/src/UiPath.Ipc.Tests/RobotTests.cs @@ -17,7 +17,7 @@ public abstract class RobotTests : TestBase protected sealed override IpcProxy? IpcProxy => Proxy as IpcProxy; protected sealed override Type ContractType => typeof(IStudioOperations); - protected override EndpointCollection? Callbacks => new() + protected override ContractCollection? Callbacks => new() { { typeof(IStudioEvents), _studioEvents } }; diff --git a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs index 5e9b11eb..2473766d 100644 --- a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs @@ -64,7 +64,7 @@ public TContract CreateUserServiceProxy(string pipeName) => RobotIpcHelpers.CreateProxy( pipeName, requestTimeout: TimeSpan.FromSeconds(40), - callbacks: new EndpointCollection() + callbacks: new ContractCollection() { { typeof(TCallback), Instance } }, @@ -79,7 +79,7 @@ internal static partial class RobotIpcHelpers public static TContract CreateProxy( string pipeName, TimeSpan? requestTimeout = null, - EndpointCollection? callbacks = null, + ContractCollection? callbacks = null, IServiceProvider? provider = null, Action? beforeConnect = null, BeforeCallHandler? beforeCall = null, @@ -199,7 +199,7 @@ 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, EndpointCollection? Callbacks) + 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(); @@ -208,11 +208,11 @@ internal readonly record struct CreateProxyRequest(Key ActualKey, Params Params, internal readonly struct EquatableEndpointSet : IEquatable { - public static EquatableEndpointSet From(EndpointCollection? endpoints, bool haveProvider) + public static EquatableEndpointSet From(ContractCollection? endpoints, bool haveProvider) { return Pal(endpoints?.AsEnumerable(), haveProvider); - static EquatableEndpointSet Pal(IEnumerable? endpoints, bool haveProvider) + static EquatableEndpointSet Pal(IEnumerable? endpoints, bool haveProvider) { var items = endpoints?.AsEnumerable(); @@ -237,9 +237,9 @@ static EquatableEndpointSet Pal(IEnumerable? endpoints, bool h } public static readonly EquatableEndpointSet Empty = new([]); - private readonly HashSet _set; + private readonly HashSet _set; - private EquatableEndpointSet(HashSet set) => _set = 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); @@ -248,7 +248,7 @@ static EquatableEndpointSet Pal(IEnumerable? endpoints, bool h public override string ToString() { return $"[{string.Join(", ", _set.Select(Pal))}]"; - static string Pal(EndpointSettings endpointSettings) + static string Pal(ContractSettings endpointSettings) => $"{endpointSettings.ContractType.Name},sp:{RuntimeHelpers.GetHashCode(endpointSettings.ServiceProvider)},instance:{RuntimeHelpers.GetHashCode(endpointSettings.ServiceInstance)}"; } } diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.Ipc.Tests/TestBase.cs index cd15246a..1318aeef 100644 --- a/src/UiPath.Ipc.Tests/TestBase.cs +++ b/src/UiPath.Ipc.Tests/TestBase.cs @@ -67,7 +67,7 @@ public TestBase(ITestOutputHelper outputHelper) protected abstract void ConfigureSpecificServices(IServiceCollection services); - protected virtual EndpointCollection? Callbacks => []; + protected virtual ContractCollection? Callbacks => []; private Task CreateIpcServer() { @@ -85,7 +85,7 @@ async Task Core() var serverTransport = await CreateServerTransport(); ConfigTransportBase(serverTransport); - var endpointSettings = new EndpointSettings(ContractType) + var endpointSettings = new ContractSettings(ContractType) { BeforeIncomingCall = (callInfo, ct) => { @@ -104,7 +104,7 @@ async Task Core() }; } } - protected IpcClient? CreateIpcClient(EndpointCollection? callbacks = null) + protected IpcClient? CreateIpcClient(ContractCollection? callbacks = null) { if (_overrideConfig is null) { From 89bb598586dfc41763d6979e6a9ae4dd08269b75 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Thu, 28 Nov 2024 10:01:07 +0100 Subject: [PATCH 12/26] update NodeInterop to the new API --- .../UiPath.CoreIpc.NodeInterop/Program.cs | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs index bb5ed827..b4476882 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.WebSockets; using System.Threading; @@ -88,23 +89,9 @@ static async Task MainCore(string? pipeName, string? webSocketUrl, int? maybeSec thread.Context.SynchronizationContext.Send(_ => Thread.CurrentThread.Name = "GuiThread", null); var scheduler = thread.Context.Scheduler; - var ipcServer = new IpcServer() - { - Endpoints = new() - { - typeof(IAlgebra), - typeof(ICalculus), - typeof(IBrittleService), - typeof(IEnvironmentVariableGetter), - typeof(IDtoService) - }, - Listeners = [ - ..EnumerateListeners(pipeName, webSocketUrl) - ], - ServiceProvider = sp, - Scheduler = scheduler - }; - ipcServer.Start(); + var ipcServers = EnumerateServerTransports(pipeName, webSocketUrl) + .Select(CreateAndStartIpcServer) + .ToArray(); _ = Task.Run(async () => { @@ -122,16 +109,13 @@ IEnumerable EnumeratePings() { yield return new IpcClient { - Config = new() - { - ServiceProvider = sp, - RequestTimeout = TimeSpan.FromHours(5), - Callbacks = new() + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() { { typeof(IArithmetic), callback } }, - }, - Transport = new WebSocketTransport + Transport = new WebSocketClientTransport { Uri = new(webSocketUrl), } @@ -144,16 +128,13 @@ IEnumerable EnumeratePings() { yield return new IpcClient { - Config = new() + ServiceProvider = sp, + RequestTimeout = TimeSpan.FromHours(5), + Callbacks = new() { - ServiceProvider = sp, - RequestTimeout = TimeSpan.FromHours(5), - Callbacks = new() - { - { typeof(IArithmetic), callback } - } + { typeof(IArithmetic), callback } }, - Transport = new NamedPipeTransport() + Transport = new NamedPipeClientTransport() { PipeName = pipeName, } @@ -174,20 +155,41 @@ IEnumerable EnumeratePings() } }); - await ipcServer.WaitForStop(); + await Task.WhenAll(ipcServers.Select(ipcServer => ipcServer.WaitForStop())); + + IpcServer CreateAndStartIpcServer(ServerTransport transport) + { + 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; + } + - IEnumerable EnumerateListeners(string? pipeName, string? webSocketUrl) + IEnumerable EnumerateServerTransports(string? pipeName, string? webSocketUrl) { if (pipeName is not null) { - yield return new NamedPipeListener() { PipeName = pipeName }; + yield return new NamedPipeServerTransport() { PipeName = pipeName }; } if (webSocketUrl is not null) { string url = CurateWebSocketUrl(webSocketUrl); var accept = new HttpSysWebSocketsListener(url).Accept; - yield return new WebSocketListener() { Accept = accept }; + yield return new WebSocketServerTransport() { Accept = accept }; } static string CurateWebSocketUrl(string raw) From 1edfc9b84c04e9914629833eef61e15d1e5b3cff Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Thu, 28 Nov 2024 12:18:34 +0100 Subject: [PATCH 13/26] simplifications after review - decommission WaitForStop --- .../UiPath.CoreIpc.NodeInterop/Program.cs | 2 +- src/UiPath.CoreIpc/Config/IpcServer.cs | 3 - .../UiPath.Ipc.net6.0-windows.received.txt | 1 - src/UiPath.Ipc.Tests/Program.cs | 58 +++++++++++++------ 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs index b4476882..cba5d22e 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/Program.cs @@ -155,7 +155,7 @@ IEnumerable EnumeratePings() } }); - await Task.WhenAll(ipcServers.Select(ipcServer => ipcServer.WaitForStop())); + await Task.Delay(Timeout.InfiniteTimeSpan); IpcServer CreateAndStartIpcServer(ServerTransport transport) { diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index 0fbc3d40..138d0ec3 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -9,7 +9,6 @@ public sealed class IpcServer : IpcBase, IAsyncDisposable private readonly object _lock = new(); private readonly TaskCompletionSource _listening = new(); - private readonly TaskCompletionSource _stopped = new(); private readonly CancellationTokenSource _ctsActiveConnections = new(); private bool _disposeStarted; @@ -70,7 +69,6 @@ public Task WaitForStart() Start(); return _accepter.StartedAccepting; } - public Task WaitForStop() => _stopped.Task; internal ILogger? CreateLogger(string category) => ServiceProvider.MaybeCreateLogger(category); @@ -82,7 +80,6 @@ private void OnNewConnection(Stream network) private void OnNewConnectionError(Exception ex) { Trace.TraceError($"Failed to accept new connection. Ex: {ex}"); - _stopped.TrySetException(ex); } internal RouterConfig CreateRouterConfig(IpcServer server) => RouterConfig.From( 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 index 95fd1bdb..1290f0c3 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -107,7 +107,6 @@ namespace UiPath.Ipc "_accepter"})] public void Start() { } public System.Threading.Tasks.Task WaitForStart() { } - public System.Threading.Tasks.Task WaitForStop() { } } public class Message { diff --git a/src/UiPath.Ipc.Tests/Program.cs b/src/UiPath.Ipc.Tests/Program.cs index feb7a67e..4f21b49b 100644 --- a/src/UiPath.Ipc.Tests/Program.cs +++ b/src/UiPath.Ipc.Tests/Program.cs @@ -4,30 +4,50 @@ using UiPath.Ipc; using UiPath.Ipc.Tests; -if (args is not [var base64]) +using (ConsoleCancellation(out var ct)) { - Console.Error.WriteLine($"Usage: dotnet {Path.GetFileName(Assembly.GetEntryAssembly()!.Location)} "); - return 1; + return await Entry(ct); } -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() +async Task Entry(CancellationToken ct) { - ServiceProvider = serviceProvider, - Scheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler, - Endpoints = new() + 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 ipcServer.WaitForStop(); + Transport = serverTransport, + }; + ipcServer.Start(); + await Task.Delay(Timeout.InfiniteTimeSpan, ct); -return 0; \ No newline at end of file + return 0; +} + +static IDisposable ConsoleCancellation(out CancellationToken ct) +{ + var cts = new CancellationTokenSource(); + ct = cts.Token; + Console.CancelKeyPress += (sender, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + return cts; +} \ No newline at end of file From d17ec528b815d7658da8127e3d7a779761e387d2 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Tue, 3 Dec 2024 09:34:29 +0100 Subject: [PATCH 14/26] reduce surface area - decommission the duplication of IClient methods in Message --- .../js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs | 4 ++-- src/Playground/Impl.cs | 4 ++-- src/UiPath.CoreIpc/Wire/Dtos.cs | 5 +---- src/UiPath.Ipc.Tests/Services/ComputingService.cs | 4 ++-- src/UiPath.Ipc.Tests/Services/Robot/Pals.cs | 2 +- src/UiPath.Ipc.Tests/Services/SystemService.cs | 6 +++--- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs index 3b9706f1..e6de1f24 100644 --- a/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs +++ b/src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/ServiceImpls.cs @@ -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/Playground/Impl.cs b/src/Playground/Impl.cs index 4d020920..66bc292d 100644 --- a/src/Playground/Impl.cs +++ b/src/Playground/Impl.cs @@ -20,8 +20,8 @@ public sealed class Server(ClientRegistry clients) : Contracts.IServerOperations { public async Task Register(Message? m = null) { - var clientOps = m!.GetCallback(); - var clientOps2 = m.GetCallback(); + var clientOps = m!.Client.GetCallback(); + var clientOps2 = m.Client.GetCallback(); var added = clients.Add(new(clientOps, clientOps2)); diff --git a/src/UiPath.CoreIpc/Wire/Dtos.cs b/src/UiPath.CoreIpc/Wire/Dtos.cs index 6f796e0c..bbb3e5da 100644 --- a/src/UiPath.CoreIpc/Wire/Dtos.cs +++ b/src/UiPath.CoreIpc/Wire/Dtos.cs @@ -7,12 +7,9 @@ namespace UiPath.Ipc; public class Message { [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(); - public void ImpersonateClient(Action action) => Client.Impersonate(action); } public class Message : Message { diff --git a/src/UiPath.Ipc.Tests/Services/ComputingService.cs b/src/UiPath.Ipc.Tests/Services/ComputingService.cs index 40f1863a..4b382d52 100644 --- a/src/UiPath.Ipc.Tests/Services/ComputingService.cs +++ b/src/UiPath.Ipc.Tests/Services/ComputingService.cs @@ -32,7 +32,7 @@ public async Task Wait(TimeSpan duration, CancellationToken ct = default) public async Task GetCallbackThreadName(TimeSpan waitOnServer, Message message = null!, CancellationToken cancellationToken = default) { await Task.Delay(waitOnServer); - return await message.GetCallback().GetThreadName(); + return await message.Client.GetCallback().GetThreadName(); } public async Task AddComplexNumberList(IReadOnlyList numbers) @@ -47,7 +47,7 @@ public async Task AddComplexNumberList(IReadOnlyList MultiplyInts(int x, int y, Message message = null!) { - var callback = message.GetCallback(); + var callback = message.Client.GetCallback(); var result = 0; for (int i = 0; i < y; i++) diff --git a/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs b/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs index 5f2daea2..cdea5968 100644 --- a/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs +++ b/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs @@ -49,7 +49,7 @@ internal sealed class Callback where T : class public Callback(Message message) { Client = message.Client; - _callback = message.GetCallback(); + _callback = message.Client.GetCallback(); } public void Invoke(Func call) => InvokeAsync(call).TraceError(); diff --git a/src/UiPath.Ipc.Tests/Services/SystemService.cs b/src/UiPath.Ipc.Tests/Services/SystemService.cs index 9f1d9394..d7cd22ca 100644 --- a/src/UiPath.Ipc.Tests/Services/SystemService.cs +++ b/src/UiPath.Ipc.Tests/Services/SystemService.cs @@ -34,7 +34,7 @@ public async Task FireAndForget(TimeSpan wait) { try { - _ = await message.GetCallback().SomeMethod(); + _ = await message.Client.GetCallback().SomeMethod(); return null; } catch (Exception ex) @@ -75,8 +75,8 @@ public async Task Download(string s, CancellationToken ct = default) public async Task AddIncrement(int x, int y, Message message = null!) { - var sum = await message.GetCallback().AddInts(x, y); - var result = await message.GetCallback().Increment(sum); + var sum = await message.Client.GetCallback().AddInts(x, y); + var result = await message.Client.GetCallback().Increment(sum); return result; } } From fdb5cd2c3fbb703a3079be53cd4b0c05ed41fcd5 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 4 Dec 2024 23:32:59 +0100 Subject: [PATCH 15/26] polyfill cleanup for net461 --- src/UiPath.CoreIpc/Config/IpcServer.cs | 3 +- .../CallerArgumentExpressionAttribute.cs | 1 - .../Polyfills/CollectionExtensions.cs | 19 -- .../Polyfills/EnumerableExtensions.cs | 2 +- .../report/UiPath.Ipc.net461.received.txt | 196 ++++++++++++++++++ .../UiPath.Ipc.net6.0-windows.received.txt | 6 - 6 files changed, 198 insertions(+), 29 deletions(-) delete mode 100644 src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs create mode 100644 src/UiPath.CoreIpc/report/UiPath.Ipc.net461.received.txt diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index 138d0ec3..74550865 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -36,7 +36,6 @@ private async Task DisposeCore() _ctsActiveConnections.Dispose(); } - [MemberNotNull(nameof(Transport), nameof(_accepter))] public void Start() { lock (_lock) @@ -67,7 +66,7 @@ public void Start() public Task WaitForStart() { Start(); - return _accepter.StartedAccepting; + return _accepter!.StartedAccepting; } internal ILogger? CreateLogger(string category) => ServiceProvider.MaybeCreateLogger(category); diff --git a/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs index 0032e3dd..45ded093 100644 --- a/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs +++ b/src/UiPath.CoreIpc/Polyfills/CallerArgumentExpressionAttribute.cs @@ -32,5 +32,4 @@ internal sealed class CallerArgumentExpressionAttribute : Attribute public string ParameterName { get; } } - #endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs b/src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs deleted file mode 100644 index c850d4a9..00000000 --- a/src/UiPath.CoreIpc/Polyfills/CollectionExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -#if NETFRAMEWORK - -namespace System.Collections.Generic; - -public static class CollectionExtensions -{ - public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) - { - if (dictionary.ContainsKey(key)) - { - return false; - } - - dictionary.Add(key, value); - return true; - } -} - -#endif \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs b/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs index 48bf84b1..dba5280f 100644 --- a/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs +++ b/src/UiPath.CoreIpc/Polyfills/EnumerableExtensions.cs @@ -2,7 +2,7 @@ namespace System.Linq; -public static class EnumerableExtensions +internal static class EnumerableExtensions { public static IEnumerable Prepend(this IEnumerable enumerable, T element) { 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 index 1290f0c3..fbe0a167 100644 --- a/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt +++ b/src/UiPath.CoreIpc/report/UiPath.Ipc.net6.0-windows.received.txt @@ -102,9 +102,6 @@ namespace UiPath.Ipc public UiPath.Ipc.ContractCollection Endpoints { get; init; } public UiPath.Ipc.ServerTransport Transport { get; init; } public System.Threading.Tasks.ValueTask DisposeAsync() { } - [System.Diagnostics.CodeAnalysis.MemberNotNull(new string[] { - "Transport", - "_accepter"})] public void Start() { } public System.Threading.Tasks.Task WaitForStart() { } } @@ -115,9 +112,6 @@ namespace UiPath.Ipc public UiPath.Ipc.IClient Client { get; set; } [Newtonsoft.Json.JsonIgnore] public System.TimeSpan RequestTimeout { get; set; } - public TCallbackInterface GetCallback() - where TCallbackInterface : class { } - public void ImpersonateClient(System.Action action) { } } public class Message : UiPath.Ipc.Message { From 72c8a718d71a56240390abc2419ae2f0764b1db2 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 9 Dec 2024 12:22:43 +0100 Subject: [PATCH 16/26] cleanup and revival --- src/CoreIpc.sln | 25 +++++-- src/IpcSample.ConsoleClient/Client.cs | 65 +++++++++--------- .../IpcSample.ConsoleClient.csproj | 11 ++-- src/IpcSample.ConsoleClient/SimpleClient.cs | 66 ------------------- src/IpcSample.ConsoleClient/TcpClient.cs | 63 ++++++++++-------- .../WebSocketClient.cs | 66 ++++++++++--------- .../IpcSample.ConsoleServer.csproj | 10 +-- src/IpcSample.ConsoleServer/Server.cs | 43 +++++++----- src/IpcSample.ConsoleServer/TcpServer.cs | 44 ++++++++----- .../WebSocketServer.cs | 48 +++++++++----- .../ServerTransportBase.cs | 4 +- ...ath.CoreIpc.Extensions.Abstractions.csproj | 2 + .../BidiHttpServerTransport.cs | 2 +- .../Constants.cs | 2 +- .../GlobalUsings.cs | 2 +- ...oreIpc.Extensions.BidirectionalHttp.csproj | 2 + .../ComputingTests.cs | 2 +- .../ComputingTestsOverNamedPipes.cs | 2 +- .../ComputingTestsOverTcp.cs | 2 +- .../ComputingTestsOverWebSockets.cs | 2 +- .../Config/OverrideConfigAttribute.cs | 2 +- .../GlobalUsings.cs | 3 +- .../Helpers/DiExtensions.cs | 2 +- .../Helpers/HttpSysWebSocketsListener.cs | 2 +- .../Helpers/IpcAutoDataAttribute.cs | 2 +- .../Helpers/IpcHelpers.cs | 2 +- .../Helpers/Names.cs | 2 +- .../Helpers/NetworkHelper.cs | 2 +- .../Helpers/ShouldlyHelpers.cs | 2 +- .../Helpers/StreamBase.cs | 2 +- .../Helpers/StreamExtensions.cs | 2 +- .../Helpers/TestRunId.cs | 2 +- .../Helpers/Timeouts.cs | 2 +- .../Helpers/TracedStream.cs | 2 +- .../Helpers/WebSocketContext.cs | 2 +- .../NamedPipeSmokeTests.cs | 2 +- .../CallerArgumentExpressionAttribute.cs | 0 .../Polyfills/IsExternalInit.cs | 0 .../Polyfills/System_Index.cs | 0 .../Program.cs | 2 +- .../RobotTests.cs | 2 +- .../RobotTestsOverNamedPipes.cs | 2 +- .../Services/ArithmeticCallback.cs | 2 +- .../Services/ComputingCallback.cs | 2 +- .../Services/ComputingService.cs | 28 ++++---- .../Services/IComputingService.cs | 7 +- .../Services/ISystemService.cs | 12 +++- .../Services/Robot/Contracts.cs | 2 +- .../Services/Robot/Impl.cs | 2 +- .../Services/Robot/Pals.cs | 2 +- .../Services/SystemService.cs | 27 +++++++- .../SpyTestBase.cs | 2 +- .../SyncOverAsyncTests.cs | 2 +- .../SystemTests.cs | 2 +- .../SystemTestsOverNamedPipes.cs | 2 +- .../SystemTestsOverTcp.cs | 2 +- .../SystemTestsOverWebSockets.cs | 2 +- .../TestBase.cs | 2 +- .../UiPath.CoreIpc.Tests.csproj} | 0 .../Xunit/CustomTestFramework.cs | 2 +- src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 5 +- 61 files changed, 320 insertions(+), 287 deletions(-) delete mode 100644 src/IpcSample.ConsoleClient/SimpleClient.cs rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/ComputingTests.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/ComputingTestsOverNamedPipes.cs (97%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/ComputingTestsOverTcp.cs (97%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/ComputingTestsOverWebSockets.cs (97%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Config/OverrideConfigAttribute.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/GlobalUsings.cs (60%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/DiExtensions.cs (94%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/HttpSysWebSocketsListener.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/IpcAutoDataAttribute.cs (91%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/IpcHelpers.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/Names.cs (84%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/NetworkHelper.cs (91%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/ShouldlyHelpers.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/StreamBase.cs (95%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/StreamExtensions.cs (95%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/TestRunId.cs (96%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/Timeouts.cs (92%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/TracedStream.cs (93%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Helpers/WebSocketContext.cs (95%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/NamedPipeSmokeTests.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Polyfills/CallerArgumentExpressionAttribute.cs (100%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Polyfills/IsExternalInit.cs (100%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Polyfills/System_Index.cs (100%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Program.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/RobotTests.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/RobotTestsOverNamedPipes.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/ArithmeticCallback.cs (77%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/ComputingCallback.cs (87%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/ComputingService.cs (84%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/IComputingService.cs (85%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/ISystemService.cs (79%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/Robot/Contracts.cs (95%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/Robot/Impl.cs (98%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/Robot/Pals.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Services/SystemService.cs (77%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SpyTestBase.cs (93%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SyncOverAsyncTests.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SystemTests.cs (99%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SystemTestsOverNamedPipes.cs (95%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SystemTestsOverTcp.cs (94%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/SystemTestsOverWebSockets.cs (96%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/TestBase.cs (99%) rename src/{UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj => UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj} (100%) rename src/{UiPath.Ipc.Tests => UiPath.CoreIpc.Tests}/Xunit/CustomTestFramework.cs (99%) diff --git a/src/CoreIpc.sln b/src/CoreIpc.sln index 2d716dc9..b17b8656 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -8,17 +8,22 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{676A208A-2F08-4749-A833-F8D2BCB1B147}" ProjectSection(SolutionItems) = preProject Directory.Build.targets = Directory.Build.targets + IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj = IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj ..\NuGet.Config = ..\NuGet.Config EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "Playground\Playground.csproj", "{F0365E40-DA73-4583-A363-89CBEF68A4C6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.Ipc.Tests", "UiPath.Ipc.Tests\UiPath.Ipc.Tests.csproj", "{E238E183-92CF-48A6-890F-C422853D6656}" -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 @@ -33,10 +38,6 @@ Global {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 - {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E238E183-92CF-48A6-890F-C422853D6656}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E238E183-92CF-48A6-890F-C422853D6656}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E238E183-92CF-48A6-890F-C422853D6656}.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 @@ -45,6 +46,18 @@ Global {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/IpcSample.ConsoleClient/Client.cs b/src/IpcSample.ConsoleClient/Client.cs index b715e1cb..e790168a 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -1,9 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Text; -using UiPath.Ipc.BackCompat; +using UiPath.Ipc; +using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; class Client { @@ -30,61 +31,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) - .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") - .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}", cancellationToken); // test 4: call IPC service method without parameter or return - await systemClient.FireAndForget(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.EchoGuid(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 @@ -118,6 +123,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 646a29aa..5b21099d 100644 --- a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj +++ b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj @@ -2,7 +2,7 @@ Exe - net7.0;net461;net7.0-windows + net6.0;net461;net6.0-windows app1.manifest latest true @@ -10,14 +10,15 @@ + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/IpcSample.ConsoleClient/SimpleClient.cs b/src/IpcSample.ConsoleClient/SimpleClient.cs deleted file mode 100644 index eba71a10..00000000 --- a/src/IpcSample.ConsoleClient/SimpleClient.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.Extensions.Logging; -using UiPath.Ipc.Tests; - -namespace IpcSample.ConsoleClient; - -internal class SimpleClient -{ - - public static async Task Entry() - { - Settings pf = new() - { - ClientTransport = new ClientTransport.NamedPipes - { - PipeName = "test", - AllowImpersonation = true - }, - Logger = new Logger(new LoggerFactory()), - RequestTimeout = TimeSpan.FromSeconds(2), - Callback = new CallbackSource.Instance - { - CallbackInstance = new ComputingCallback { Id = "custom made" } - } - }; - - var cs = pf.Build(); - // ----------- - } - - class Settings - { - public required ClientTransport ClientTransport { get; init; } - public TimeSpan RequestTimeout { get; init; } = Timeout.InfiniteTimeSpan; - public required ILogger Logger { get; init; } - public CallbackSource? Callback { get; init; } - - public T Build() where T : class - { - throw null!; - } - } - - abstract class CallbackSource - { - public class Injected : CallbackSource - { - public required IServiceProvider ServiceProvider { get; init; } - public required Type CallbackType { get; init; } - } - - public class Instance : CallbackSource - { - public required object CallbackInstance { get; init; } - } - } - - abstract class ClientTransport - { - public class NamedPipes : ClientTransport - { - public required string PipeName { get; init; } - public bool AllowImpersonation { get; init; } - } - } - -} diff --git a/src/IpcSample.ConsoleClient/TcpClient.cs b/src/IpcSample.ConsoleClient/TcpClient.cs index 0e5b33ed..7dfecb72 100644 --- a/src/IpcSample.ConsoleClient/TcpClient.cs +++ b/src/IpcSample.ConsoleClient/TcpClient.cs @@ -1,10 +1,11 @@ using System.Text; using System.Diagnostics; -using UiPath.Ipc.BackCompat; using Microsoft.Extensions.DependencyInjection; using System.Net; +using UiPath.Ipc; +using UiPath.Ipc.Transport.Tcp; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; class TcpClient { @@ -33,45 +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) - .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) - //.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); @@ -79,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.EchoGuid(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 @@ -92,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 @@ -130,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 c9e1afbc..d0e87bb3 100644 --- a/src/IpcSample.ConsoleClient/WebSocketClient.cs +++ b/src/IpcSample.ConsoleClient/WebSocketClient.cs @@ -1,9 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Text; -using UiPath.Ipc.BackCompat; +using UiPath.Ipc; +using UiPath.Ipc.Transport.WebSocket; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; class WebSocketClient { @@ -33,45 +34,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) - .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) - //.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); @@ -79,11 +83,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.EchoGuid(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 @@ -92,7 +96,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 @@ -113,8 +117,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 { @@ -130,6 +134,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..9b4697e4 100644 --- a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj +++ b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj @@ -2,7 +2,7 @@ Exe - net7.0;net461;net7.0-windows + net6.0;net461;net6.0-windows app1.manifest latest true @@ -14,10 +14,10 @@ - - - - + + + + diff --git a/src/IpcSample.ConsoleServer/Server.cs b/src/IpcSample.ConsoleServer/Server.cs index 87e6c9ca..a2c88b4b 100644 --- a/src/IpcSample.ConsoleServer/Server.cs +++ b/src/IpcSample.ConsoleServer/Server.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc.BackCompat; +using UiPath.Ipc; using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; class Server { @@ -21,30 +21,39 @@ static async Task Main() Console.WriteLine(SynchronizationContext.Current); var serviceProvider = ConfigureServices(); // build and run service host - var host = new ServiceHostBuilder(serviceProvider) - .UseNamedPipes(new NamedPipeListener() + + await using var ipcServer = new IpcServer + { + Transport = new NamedPipeServerTransport { PipeName = "test" }, + ServiceProvider = serviceProvider, + Endpoints = new() { - PipeName = "test", - 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(async () => + var tcs = new TaskCompletionSource(); + Console.CancelKeyPress += (_, e) => { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - await host.DisposeAsync(); - })); + 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 a89cec24..5003ce55 100644 --- a/src/IpcSample.ConsoleServer/TcpServer.cs +++ b/src/IpcSample.ConsoleServer/TcpServer.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc.BackCompat; +using UiPath.Ipc; using UiPath.Ipc.Transport.Tcp; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; using IPEndPoint = System.Net.IPEndPoint; using IPAddress = System.Net.IPAddress; @@ -27,30 +27,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 TcpListener() + + await using var ipcServer = new IpcServer + { + Transport = new TcpServerTransport { EndPoint = SystemEndPoint }, + ServiceProvider = serviceProvider, + Endpoints = new() { - EndPoint = SystemEndPoint, - RequestTimeout = TimeSpan.FromSeconds(2), - //Certificate = new X509Certificate(data, "1"), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - - await await Task.WhenAny(host.RunAsync(), Task.Run(async () => + 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(); - await host.DisposeAsync(); - })); + 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 8a52c013..098cc22a 100644 --- a/src/IpcSample.ConsoleServer/WebSocketServer.cs +++ b/src/IpcSample.ConsoleServer/WebSocketServer.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc.BackCompat; +using UiPath.Ipc; using UiPath.Ipc.Transport.WebSocket; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; class WebSocketServer { //private static readonly Timer _timer = new Timer(_ => @@ -21,28 +21,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 WebSocketListener() + + await using var ipcServer = new IpcServer + { + Transport = new WebSocketServerTransport { Accept = new HttpSysWebSocketsListener("http://localhost:1212/wsDemo/").Accept, - RequestTimeout = TimeSpan.FromSeconds(2), - //Certificate = new X509Certificate(data, "1"), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - await await Task.WhenAny(host.RunAsync(), Task.Run(async () => + }, + ServiceProvider = serviceProvider, + Endpoints = new() + { + 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(); - await host.DisposeAsync(); - })); + 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/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs index 6fd88769..4d522c11 100644 --- a/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/ServerTransportBase.cs @@ -1,6 +1,4 @@ -using UiPath.Ipc; - -namespace UiPath.CoreIpc.Extensions.Abstractions; +namespace UiPath.Ipc.Extensions.Abstractions; public abstract class ServerTransportBase : ServerTransport { diff --git a/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj index 2186c679..e1b2de66 100644 --- a/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj @@ -8,6 +8,8 @@ true enable true + 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 index 7b536b1d..223e76a4 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs @@ -7,7 +7,7 @@ using System.Net.Http; using System.Threading.Channels; -namespace UiPath.CoreIpc.Extensions.BidirectionalHttp; +namespace UiPath.Ipc.Extensions.BidirectionalHttp; using static Constants; diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs index 43abe56d..e24c8211 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Constants.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Extensions.BidirectionalHttp; +namespace UiPath.Ipc.Extensions.BidirectionalHttp; internal static class Constants { diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs index d502d37a..403f1379 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/GlobalUsings.cs @@ -1 +1 @@ -global using UiPath.CoreIpc.Extensions.Abstractions; +global using UiPath.Ipc.Extensions.Abstractions; diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj index 83f0f21e..81d35dd7 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj @@ -5,6 +5,8 @@ enable enable preview + UiPath.Ipc.Extensions.BidirectionalHttp + UiPath.Ipc.Extensions.BidirectionalHttp diff --git a/src/UiPath.Ipc.Tests/ComputingTests.cs b/src/UiPath.CoreIpc.Tests/ComputingTests.cs similarity index 99% rename from src/UiPath.Ipc.Tests/ComputingTests.cs rename to src/UiPath.CoreIpc.Tests/ComputingTests.cs index 2d7b1d86..c7404a7c 100644 --- a/src/UiPath.Ipc.Tests/ComputingTests.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTests.cs @@ -9,7 +9,7 @@ using UiPath.Ipc.Transport.WebSocket; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public abstract class ComputingTests : SpyTestBase { diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs similarity index 97% rename from src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs rename to src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs index 88ef854f..3b463d1c 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ComputingTestsOverNamedPipes : ComputingTests { diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs similarity index 97% rename from src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs rename to src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs index 12cec28c..96233fda 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverTcp.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverTcp.cs @@ -2,7 +2,7 @@ using UiPath.Ipc.Transport.Tcp; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ComputingTestsOverTcp : ComputingTests { diff --git a/src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs similarity index 97% rename from src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs rename to src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs index 3b6022aa..4df0126d 100644 --- a/src/UiPath.Ipc.Tests/ComputingTestsOverWebSockets.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverWebSockets.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.WebSocket; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ComputingTestsOverWebSockets : ComputingTests { diff --git a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs similarity index 98% rename from src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs rename to src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs index cecdedde..8144dab2 100644 --- a/src/UiPath.Ipc.Tests/Config/OverrideConfigAttribute.cs +++ b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] internal sealed class OverrideConfigAttribute : Attribute diff --git a/src/UiPath.Ipc.Tests/GlobalUsings.cs b/src/UiPath.CoreIpc.Tests/GlobalUsings.cs similarity index 60% rename from src/UiPath.Ipc.Tests/GlobalUsings.cs rename to src/UiPath.CoreIpc.Tests/GlobalUsings.cs index 364e8e60..74b2b413 100644 --- a/src/UiPath.Ipc.Tests/GlobalUsings.cs +++ b/src/UiPath.CoreIpc.Tests/GlobalUsings.cs @@ -1,3 +1,4 @@ -global using Accept = System.Func>; +global using UiPath.Ipc; +global using Accept = System.Func>; global using BeforeConnectHandler = System.Func; global using BeforeCallHandler = System.Func; diff --git a/src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs similarity index 94% rename from src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs rename to src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs index fd750042..6af0db10 100644 --- a/src/UiPath.Ipc.Tests/Helpers/DiExtensions.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Hosting; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public static class DiExtensions { diff --git a/src/UiPath.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs similarity index 98% rename from src/UiPath.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs rename to src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs index 257e9a8b..3745702b 100644 --- a/src/UiPath.Ipc.Tests/Helpers/HttpSysWebSocketsListener.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs @@ -2,7 +2,7 @@ using System.Net; using System.Threading.Channels; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public class HttpSysWebSocketsListener : IAsyncDisposable { diff --git a/src/UiPath.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs similarity index 91% rename from src/UiPath.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs rename to src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs index 2ce875a1..58184388 100644 --- a/src/UiPath.Ipc.Tests/Helpers/IpcAutoDataAttribute.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs @@ -1,7 +1,7 @@ using AutoFixture; using AutoFixture.Xunit2; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal class IpcAutoDataAttribute : AutoDataAttribute { diff --git a/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs similarity index 98% rename from src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs rename to src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs index 1f0c1217..e100cc8e 100644 --- a/src/UiPath.Ipc.Tests/Helpers/IpcHelpers.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; using SP = ServiceProviderServiceExtensions; diff --git a/src/UiPath.Ipc.Tests/Helpers/Names.cs b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs similarity index 84% rename from src/UiPath.Ipc.Tests/Helpers/Names.cs rename to src/UiPath.CoreIpc.Tests/Helpers/Names.cs index 2668055b..f3e0f82d 100644 --- a/src/UiPath.Ipc.Tests/Helpers/Names.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal static class Names { diff --git a/src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs similarity index 91% rename from src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs rename to src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs index 327ffbd1..e346a349 100644 --- a/src/UiPath.Ipc.Tests/Helpers/NetworkHelper.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Sockets; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public static class NetworkHelper { diff --git a/src/UiPath.Ipc.Tests/Helpers/ShouldlyHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs similarity index 99% rename from src/UiPath.Ipc.Tests/Helpers/ShouldlyHelpers.cs rename to src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs index 0895c94d..f4bdfdaa 100644 --- a/src/UiPath.Ipc.Tests/Helpers/ShouldlyHelpers.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; [ShouldlyMethods] internal static class ShouldlyHelpers diff --git a/src/UiPath.Ipc.Tests/Helpers/StreamBase.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs similarity index 95% rename from src/UiPath.Ipc.Tests/Helpers/StreamBase.cs rename to src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs index cbf2c599..a4748d38 100644 --- a/src/UiPath.Ipc.Tests/Helpers/StreamBase.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal abstract class StreamBase : Stream { diff --git a/src/UiPath.Ipc.Tests/Helpers/StreamExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs similarity index 95% rename from src/UiPath.Ipc.Tests/Helpers/StreamExtensions.cs rename to src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs index 82174e14..8c7c5e2d 100644 --- a/src/UiPath.Ipc.Tests/Helpers/StreamExtensions.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public static class StreamExtensions { diff --git a/src/UiPath.Ipc.Tests/Helpers/TestRunId.cs b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs similarity index 96% rename from src/UiPath.Ipc.Tests/Helpers/TestRunId.cs rename to src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs index f76d43c8..4bf36318 100644 --- a/src/UiPath.Ipc.Tests/Helpers/TestRunId.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public readonly record struct TestRunId(Guid Value) { diff --git a/src/UiPath.Ipc.Tests/Helpers/Timeouts.cs b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs similarity index 92% rename from src/UiPath.Ipc.Tests/Helpers/Timeouts.cs rename to src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs index d09e1e2e..8202d755 100644 --- a/src/UiPath.Ipc.Tests/Helpers/Timeouts.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal static class Timeouts { diff --git a/src/UiPath.Ipc.Tests/Helpers/TracedStream.cs b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs similarity index 93% rename from src/UiPath.Ipc.Tests/Helpers/TracedStream.cs rename to src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs index e9648056..b672bccf 100644 --- a/src/UiPath.Ipc.Tests/Helpers/TracedStream.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal sealed class TracedStream(Stream target) : StreamBase { diff --git a/src/UiPath.Ipc.Tests/Helpers/WebSocketContext.cs b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs similarity index 95% rename from src/UiPath.Ipc.Tests/Helpers/WebSocketContext.cs rename to src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs index 5fb55217..846aeda9 100644 --- a/src/UiPath.Ipc.Tests/Helpers/WebSocketContext.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal sealed class WebSocketContext : IAsyncDisposable { diff --git a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs similarity index 98% rename from src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs rename to src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs index e43eee21..2fe65006 100644 --- a/src/UiPath.Ipc.Tests/NamedPipeSmokeTests.cs +++ b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs @@ -1,6 +1,6 @@ using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class NamedPipeSmokeTests { diff --git a/src/UiPath.Ipc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs b/src/UiPath.CoreIpc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs similarity index 100% rename from src/UiPath.Ipc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs rename to src/UiPath.CoreIpc.Tests/Polyfills/CallerArgumentExpressionAttribute.cs diff --git a/src/UiPath.Ipc.Tests/Polyfills/IsExternalInit.cs b/src/UiPath.CoreIpc.Tests/Polyfills/IsExternalInit.cs similarity index 100% rename from src/UiPath.Ipc.Tests/Polyfills/IsExternalInit.cs rename to src/UiPath.CoreIpc.Tests/Polyfills/IsExternalInit.cs diff --git a/src/UiPath.Ipc.Tests/Polyfills/System_Index.cs b/src/UiPath.CoreIpc.Tests/Polyfills/System_Index.cs similarity index 100% rename from src/UiPath.Ipc.Tests/Polyfills/System_Index.cs rename to src/UiPath.CoreIpc.Tests/Polyfills/System_Index.cs diff --git a/src/UiPath.Ipc.Tests/Program.cs b/src/UiPath.CoreIpc.Tests/Program.cs similarity index 98% rename from src/UiPath.Ipc.Tests/Program.cs rename to src/UiPath.CoreIpc.Tests/Program.cs index 4f21b49b..b2f15293 100644 --- a/src/UiPath.Ipc.Tests/Program.cs +++ b/src/UiPath.CoreIpc.Tests/Program.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using System.Text; using UiPath.Ipc; -using UiPath.Ipc.Tests; +using UiPath.CoreIpc.Tests; using (ConsoleCancellation(out var ct)) { diff --git a/src/UiPath.Ipc.Tests/RobotTests.cs b/src/UiPath.CoreIpc.Tests/RobotTests.cs similarity index 98% rename from src/UiPath.Ipc.Tests/RobotTests.cs rename to src/UiPath.CoreIpc.Tests/RobotTests.cs index bc173e51..29d6811a 100644 --- a/src/UiPath.Ipc.Tests/RobotTests.cs +++ b/src/UiPath.CoreIpc.Tests/RobotTests.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public abstract class RobotTests : TestBase { diff --git a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs similarity index 99% rename from src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs rename to src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs index 2473766d..3cb2ad4b 100644 --- a/src/UiPath.Ipc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs @@ -4,7 +4,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class RobotTestsOverNamedPipes : RobotTests { diff --git a/src/UiPath.Ipc.Tests/Services/ArithmeticCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs similarity index 77% rename from src/UiPath.Ipc.Tests/Services/ArithmeticCallback.cs rename to src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs index 4493a934..0e2f2e8c 100644 --- a/src/UiPath.Ipc.Tests/Services/ArithmeticCallback.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ArithmeticCallback : IArithmeticCallback { diff --git a/src/UiPath.Ipc.Tests/Services/ComputingCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs similarity index 87% rename from src/UiPath.Ipc.Tests/Services/ComputingCallback.cs rename to src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs index 630a8c71..7c3df4b3 100644 --- a/src/UiPath.Ipc.Tests/Services/ComputingCallback.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ComputingCallback : IComputingCallback { diff --git a/src/UiPath.Ipc.Tests/Services/ComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs similarity index 84% rename from src/UiPath.Ipc.Tests/Services/ComputingService.cs rename to src/UiPath.CoreIpc.Tests/Services/ComputingService.cs index 4b382d52..9d174fc8 100644 --- a/src/UiPath.Ipc.Tests/Services/ComputingService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class ComputingService(ILogger logger) : IComputingService { @@ -17,11 +17,20 @@ public async Task AddFloats(float a, float b, CancellationToken ct = defa return a + b; } - public async Task AddComplexNumbers(ComplexNumber a, ComplexNumber 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) { @@ -35,16 +44,6 @@ public async Task GetCallbackThreadName(TimeSpan waitOnServer, Message m return await message.Client.GetCallback().GetThreadName(); } - public async Task AddComplexNumberList(IReadOnlyList numbers) - { - var result = ComplexNumber.Zero; - foreach (var number in numbers) - { - result += number; - } - return result; - } - public async Task MultiplyInts(int x, int y, Message message = null!) { var callback = message.Client.GetCallback(); @@ -63,4 +62,9 @@ public async Task MultiplyInts(int x, int y, Message message = null!) 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.Ipc.Tests/Services/IComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs similarity index 85% rename from src/UiPath.Ipc.Tests/Services/IComputingService.cs rename to src/UiPath.CoreIpc.Tests/Services/IComputingService.cs index 145c00f6..d3ef2c34 100644 --- a/src/UiPath.Ipc.Tests/Services/IComputingService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs @@ -1,5 +1,5 @@  -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public interface IComputingServiceBase { @@ -8,12 +8,13 @@ public interface IComputingServiceBase public interface IComputingService : IComputingServiceBase { - Task AddComplexNumbers(ComplexNumber a, ComplexNumber b); + 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 AddComplexNumberList(IReadOnlyList numbers); Task MultiplyInts(int x, int y, Message message = null!); Task GetCallContext(); + Task SendMessage(Message m = null!, CancellationToken ct = default); } public interface IComputingCallbackBase diff --git a/src/UiPath.Ipc.Tests/Services/ISystemService.cs b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs similarity index 79% rename from src/UiPath.Ipc.Tests/Services/ISystemService.cs rename to src/UiPath.CoreIpc.Tests/Services/ISystemService.cs index 06714a5f..93a096ba 100644 --- a/src/UiPath.Ipc.Tests/Services/ISystemService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public interface ISystemService { @@ -23,6 +23,12 @@ public interface ISystemService /// 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!); @@ -37,6 +43,10 @@ public interface ISystemService 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 diff --git a/src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs similarity index 95% rename from src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs rename to src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs index 2469b06a..82068083 100644 --- a/src/UiPath.Ipc.Tests/Services/Robot/Contracts.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs @@ -1,4 +1,4 @@ -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public interface IStudioOperations : IStudioAgentOperations { diff --git a/src/UiPath.Ipc.Tests/Services/Robot/Impl.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs similarity index 98% rename from src/UiPath.Ipc.Tests/Services/Robot/Impl.cs rename to src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs index f02fdea9..341385ba 100644 --- a/src/UiPath.Ipc.Tests/Services/Robot/Impl.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs @@ -1,7 +1,7 @@ using Nito.AsyncEx; using Nito.Disposables; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public class StudioOperations : IStudioOperations { diff --git a/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs similarity index 99% rename from src/UiPath.Ipc.Tests/Services/Robot/Pals.cs rename to src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs index cdea5968..8f11e63b 100644 --- a/src/UiPath.Ipc.Tests/Services/Robot/Pals.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs @@ -1,7 +1,7 @@ using Nito.AsyncEx; using System.Runtime.CompilerServices; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; internal sealed class Callbacks where T : class { diff --git a/src/UiPath.Ipc.Tests/Services/SystemService.cs b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs similarity index 77% rename from src/UiPath.Ipc.Tests/Services/SystemService.cs rename to src/UiPath.CoreIpc.Tests/Services/SystemService.cs index d7cd22ca..15299a24 100644 --- a/src/UiPath.Ipc.Tests/Services/SystemService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs @@ -1,7 +1,9 @@ -using System.Buffers; +using Castle.Core; +using System.Buffers; +using System.Globalization; using System.Text; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class SystemService : ISystemService { @@ -79,4 +81,25 @@ public async Task AddIncrement(int x, int y, Message message = null!) 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.Ipc.Tests/SpyTestBase.cs b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs similarity index 93% rename from src/UiPath.Ipc.Tests/SpyTestBase.cs rename to src/UiPath.CoreIpc.Tests/SpyTestBase.cs index 2fc866f3..f52ea16d 100644 --- a/src/UiPath.Ipc.Tests/SpyTestBase.cs +++ b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public abstract class SpyTestBase : TestBase { diff --git a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs similarity index 99% rename from src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs rename to src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs index 6992eeeb..1298d46a 100644 --- a/src/UiPath.Ipc.Tests/SyncOverAsyncTests.cs +++ b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs @@ -1,6 +1,6 @@ using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public class SyncOverAsyncTests { diff --git a/src/UiPath.Ipc.Tests/SystemTests.cs b/src/UiPath.CoreIpc.Tests/SystemTests.cs similarity index 99% rename from src/UiPath.Ipc.Tests/SystemTests.cs rename to src/UiPath.CoreIpc.Tests/SystemTests.cs index 70db74df..e00389df 100644 --- a/src/UiPath.Ipc.Tests/SystemTests.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTests.cs @@ -5,7 +5,7 @@ using System.Threading.Channels; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public abstract class SystemTests : TestBase { diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs similarity index 95% rename from src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs rename to src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs index 27f83f70..cb84c84a 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class SystemTestsOverNamedPipes : SystemTests { diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs similarity index 94% rename from src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs rename to src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs index d9878b51..161aa3e5 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverTcp.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs @@ -2,7 +2,7 @@ using UiPath.Ipc.Transport.Tcp; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class SystemTestsOverTcp : SystemTests { diff --git a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs similarity index 96% rename from src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs rename to src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs index bc17f6f6..f32bdf85 100644 --- a/src/UiPath.Ipc.Tests/SystemTestsOverWebSockets.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.WebSocket; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public sealed class SystemTestsOverWebSockets : SystemTests { diff --git a/src/UiPath.Ipc.Tests/TestBase.cs b/src/UiPath.CoreIpc.Tests/TestBase.cs similarity index 99% rename from src/UiPath.Ipc.Tests/TestBase.cs rename to src/UiPath.CoreIpc.Tests/TestBase.cs index 1318aeef..4439f3ab 100644 --- a/src/UiPath.Ipc.Tests/TestBase.cs +++ b/src/UiPath.CoreIpc.Tests/TestBase.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using Xunit.Abstractions; -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public abstract class TestBase : IAsyncLifetime { diff --git a/src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj similarity index 100% rename from src/UiPath.Ipc.Tests/UiPath.Ipc.Tests.csproj rename to src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj diff --git a/src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs similarity index 99% rename from src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs rename to src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs index b954340b..32de313e 100644 --- a/src/UiPath.Ipc.Tests/Xunit/CustomTestFramework.cs +++ b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs @@ -3,7 +3,7 @@ [assembly: TestFramework(typeName: "UiPath.Ipc.Tests.CustomTestFramework", assemblyName: "UiPath.Ipc.Tests")] -namespace UiPath.Ipc.Tests; +namespace UiPath.CoreIpc.Tests; public readonly struct CustomTestContext { diff --git a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index 85014d33..fa462395 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -41,6 +41,7 @@ + @@ -55,8 +56,4 @@ - - - - \ No newline at end of file From f4bffbbb7e5914b1e9734e0a075153746bbb1805 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Thu, 12 Dec 2024 12:33:16 +0100 Subject: [PATCH 17/26] warning fixes, namespace and polyfill cleanup --- src/CoreIpc.sln | 1 + src/Directory.Build.props | 5 + src/IpcSample.ConsoleClient/Client.cs | 7 +- .../IpcSample.ConsoleClient.csproj | 4 - src/IpcSample.ConsoleClient/TcpClient.cs | 14 +- .../WebSocketClient.cs | 7 +- .../IpcSample.ConsoleServer.csproj | 1 + src/IpcSample.ConsoleServer/Server.cs | 14 +- src/IpcSample.ConsoleServer/TcpServer.cs | 16 +- .../WebSocketServer.cs | 13 +- .../BidiHttpServerTransport.cs | 2 +- .../CallerArgumentExpressionAttribute.cs | 35 ++++ .../Polyfills/CancellationTokenExtensions.cs | 11 ++ .../CompilerFeatureRequiredAttribute.cs | 56 +++++++ .../Polyfills/EnumerableExtensions.cs | 17 ++ .../Polyfills/IsExternalInit.cs | 19 +++ .../Polyfills/MemberNotNullAttribute.cs | 71 ++++++++ .../Polyfills/MemberNotNullWhenAttribute.cs | 38 +++++ .../Polyfills/NotNullIfNotNullAttribute.cs | 41 +++++ .../Polyfills/NotNullWhenAttribute.cs | 38 +++++ .../Polyfills/RequiredMemberAttribute.cs | 29 ++++ .../Polyfills/SetsRequiredMembersAttribute.cs | 20 +++ .../Polyfills/StreamExtensions.cs | 43 +++++ .../Polyfills/System_Index.cs | 151 ++++++++++++++++++ .../Polyfills/TcpClientExtensions.cs | 14 ++ ...oreIpc.Extensions.BidirectionalHttp.csproj | 4 - src/UiPath.CoreIpc.Tests/ComputingTests.cs | 4 +- .../ComputingTestsOverNamedPipes.cs | 2 +- .../Config/OverrideConfigAttribute.cs | 2 +- .../Helpers/DiExtensions.cs | 2 +- .../Helpers/HttpSysWebSocketsListener.cs | 2 +- .../Helpers/IpcAutoDataAttribute.cs | 2 +- .../Helpers/IpcHelpers.cs | 2 +- src/UiPath.CoreIpc.Tests/Helpers/Names.cs | 2 +- .../Helpers/NetworkHelper.cs | 2 +- .../Helpers/ShouldlyHelpers.cs | 2 +- .../Helpers/StreamBase.cs | 2 +- .../Helpers/StreamExtensions.cs | 2 +- src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs | 2 +- src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs | 2 +- .../Helpers/TracedStream.cs | 2 +- .../Helpers/WebSocketContext.cs | 2 +- .../NamedPipeSmokeTests.cs | 2 +- src/UiPath.CoreIpc.Tests/Program.cs | 87 +++++----- src/UiPath.CoreIpc.Tests/RobotTests.cs | 2 +- .../RobotTestsOverNamedPipes.cs | 6 +- .../Services/ArithmeticCallback.cs | 2 +- .../Services/ComputingCallback.cs | 2 +- .../Services/ComputingService.cs | 2 +- .../Services/IComputingService.cs | 2 +- .../Services/ISystemService.cs | 2 +- .../Services/Robot/Contracts.cs | 2 +- .../Services/Robot/Impl.cs | 2 +- .../Services/Robot/Pals.cs | 9 +- .../Services/SystemService.cs | 2 +- src/UiPath.CoreIpc.Tests/SpyTestBase.cs | 2 +- .../SyncOverAsyncTests.cs | 2 +- src/UiPath.CoreIpc.Tests/SystemTests.cs | 2 +- .../SystemTestsOverNamedPipes.cs | 2 +- .../SystemTestsOverTcp.cs | 2 +- .../SystemTestsOverWebSockets.cs | 2 +- src/UiPath.CoreIpc.Tests/TestBase.cs | 2 +- .../UiPath.CoreIpc.Tests.csproj | 2 + .../Xunit/CustomTestFramework.cs | 2 +- src/UiPath.CoreIpc/Helpers/Helpers.cs | 5 +- .../Helpers/TaskCompletionPool.cs | 2 +- src/UiPath.CoreIpc/Server/ServerConnection.cs | 2 +- 67 files changed, 711 insertions(+), 143 deletions(-) create mode 100644 src/Directory.Build.props create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CallerArgumentExpressionAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CancellationTokenExtensions.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/CompilerFeatureRequiredAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/EnumerableExtensions.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/IsExternalInit.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/MemberNotNullWhenAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullIfNotNullAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/NotNullWhenAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/RequiredMemberAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/SetsRequiredMembersAttribute.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/StreamExtensions.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/System_Index.cs create mode 100644 src/UiPath.CoreIpc.Extensions.BidirectionalHttp/Polyfills/TcpClientExtensions.cs diff --git a/src/CoreIpc.sln b/src/CoreIpc.sln index b17b8656..297e2730 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UiPath.CoreIpc", "UiPath.Co EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{676A208A-2F08-4749-A833-F8D2BCB1B147}" ProjectSection(SolutionItems) = preProject + 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 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/IpcSample.ConsoleClient/Client.cs b/src/IpcSample.ConsoleClient/Client.cs index e790168a..2ab2cba6 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -1,14 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Text; -using UiPath.Ipc; 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)); diff --git a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj index 5b21099d..661d8ca9 100644 --- a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj +++ b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj @@ -19,9 +19,5 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/IpcSample.ConsoleClient/TcpClient.cs b/src/IpcSample.ConsoleClient/TcpClient.cs index 7dfecb72..05a56a97 100644 --- a/src/IpcSample.ConsoleClient/TcpClient.cs +++ b/src/IpcSample.ConsoleClient/TcpClient.cs @@ -2,15 +2,15 @@ using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using System.Net; -using UiPath.Ipc; 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)); @@ -118,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 { diff --git a/src/IpcSample.ConsoleClient/WebSocketClient.cs b/src/IpcSample.ConsoleClient/WebSocketClient.cs index d0e87bb3..394c469e 100644 --- a/src/IpcSample.ConsoleClient/WebSocketClient.cs +++ b/src/IpcSample.ConsoleClient/WebSocketClient.cs @@ -1,14 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Text; -using UiPath.Ipc; using UiPath.Ipc.Transport.WebSocket; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; -class WebSocketClient +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)); diff --git a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj index 9b4697e4..bd1e52fa 100644 --- a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj +++ b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj @@ -6,6 +6,7 @@ app1.manifest latest true + enable diff --git a/src/IpcSample.ConsoleServer/Server.cs b/src/IpcSample.ConsoleServer/Server.cs index a2c88b4b..3f77031b 100644 --- a/src/IpcSample.ConsoleServer/Server.cs +++ b/src/IpcSample.ConsoleServer/Server.cs @@ -1,20 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc; 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(); diff --git a/src/IpcSample.ConsoleServer/TcpServer.cs b/src/IpcSample.ConsoleServer/TcpServer.cs index 5003ce55..88b3a9b9 100644 --- a/src/IpcSample.ConsoleServer/TcpServer.cs +++ b/src/IpcSample.ConsoleServer/TcpServer.cs @@ -1,25 +1,17 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc; using UiPath.Ipc.Transport.Tcp; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; using IPEndPoint = System.Net.IPEndPoint; using IPAddress = System.Net.IPAddress; -class TcpServer +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); + private static readonly IPEndPoint SystemEndPoint = new(IPAddress.Any, 3131); - static async Task _Main() + public static async Task _Main() { Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); //GuiLikeSyncContext.Install(); diff --git a/src/IpcSample.ConsoleServer/WebSocketServer.cs b/src/IpcSample.ConsoleServer/WebSocketServer.cs index 098cc22a..60244ed0 100644 --- a/src/IpcSample.ConsoleServer/WebSocketServer.cs +++ b/src/IpcSample.ConsoleServer/WebSocketServer.cs @@ -1,19 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using UiPath.Ipc; using UiPath.Ipc.Transport.WebSocket; -namespace UiPath.CoreIpc.Tests; +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(); diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs index 223e76a4..190b0ceb 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/BidiHttpServerTransport.cs @@ -13,7 +13,7 @@ namespace UiPath.Ipc.Extensions.BidirectionalHttp; public sealed partial class BidiHttpServerTransport : ServerTransportBase { - public required Uri Uri { get; init; } + public required Uri Uri { get; set; } protected override ServerState CreateState() => new BidiHttpServerState(this); 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 index 81d35dd7..8e55878f 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj @@ -12,10 +12,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/UiPath.CoreIpc.Tests/ComputingTests.cs b/src/UiPath.CoreIpc.Tests/ComputingTests.cs index c7404a7c..3fc1542a 100644 --- a/src/UiPath.CoreIpc.Tests/ComputingTests.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTests.cs @@ -9,7 +9,7 @@ using UiPath.Ipc.Transport.WebSocket; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public abstract class ComputingTests : SpyTestBase { @@ -156,7 +156,7 @@ public async Task BeforeConnect_ShouldWork() await Proxy.AddFloats(1, 2).ShouldBeAsync(3); callCount.ShouldBe(1); - await IpcProxy.CloseConnection(); + await (IpcProxy?.CloseConnection() ?? default); await Proxy.AddFloats(1, 2).ShouldBeAsync(3); callCount.ShouldBe(2); } diff --git a/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs index 3b463d1c..88ef854f 100644 --- a/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTestsOverNamedPipes.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class ComputingTestsOverNamedPipes : ComputingTests { diff --git a/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs index 8144dab2..cecdedde 100644 --- a/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs +++ b/src/UiPath.CoreIpc.Tests/Config/OverrideConfigAttribute.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] internal sealed class OverrideConfigAttribute : Attribute diff --git a/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs index 6af0db10..fd750042 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/DiExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Hosting; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public static class DiExtensions { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs index 3745702b..257e9a8b 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/HttpSysWebSocketsListener.cs @@ -2,7 +2,7 @@ using System.Net; using System.Threading.Channels; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public class HttpSysWebSocketsListener : IAsyncDisposable { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs index 58184388..2ce875a1 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcAutoDataAttribute.cs @@ -1,7 +1,7 @@ using AutoFixture; using AutoFixture.Xunit2; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal class IpcAutoDataAttribute : AutoDataAttribute { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs index e100cc8e..1f0c1217 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/IpcHelpers.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; using SP = ServiceProviderServiceExtensions; diff --git a/src/UiPath.CoreIpc.Tests/Helpers/Names.cs b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs index f3e0f82d..2668055b 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/Names.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/Names.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal static class Names { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs index e346a349..327ffbd1 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/NetworkHelper.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Sockets; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public static class NetworkHelper { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs index f4bdfdaa..0895c94d 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; [ShouldlyMethods] internal static class ShouldlyHelpers diff --git a/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs index a4748d38..cbf2c599 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamBase.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal abstract class StreamBase : Stream { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs index 8c7c5e2d..82174e14 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/StreamExtensions.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public static class StreamExtensions { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs index 4bf36318..f76d43c8 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/TestRunId.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public readonly record struct TestRunId(Guid Value) { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs index 8202d755..d09e1e2e 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/Timeouts.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal static class Timeouts { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs index b672bccf..e9648056 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/TracedStream.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal sealed class TracedStream(Stream target) : StreamBase { diff --git a/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs index 846aeda9..5fb55217 100644 --- a/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs +++ b/src/UiPath.CoreIpc.Tests/Helpers/WebSocketContext.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal sealed class WebSocketContext : IAsyncDisposable { diff --git a/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs index 2fe65006..e43eee21 100644 --- a/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs +++ b/src/UiPath.CoreIpc.Tests/NamedPipeSmokeTests.cs @@ -1,6 +1,6 @@ using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class NamedPipeSmokeTests { diff --git a/src/UiPath.CoreIpc.Tests/Program.cs b/src/UiPath.CoreIpc.Tests/Program.cs index b2f15293..73419438 100644 --- a/src/UiPath.CoreIpc.Tests/Program.cs +++ b/src/UiPath.CoreIpc.Tests/Program.cs @@ -1,53 +1,60 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Text; -using UiPath.Ipc; -using UiPath.CoreIpc.Tests; -using (ConsoleCancellation(out var ct)) -{ - return await Entry(ct); -} +namespace UiPath.Ipc.Tests; -async Task Entry(CancellationToken ct) +internal static class Program { - if (args is not [var base64]) + public static async Task Main(string[] args) { - Console.Error.WriteLine($"Usage: dotnet {Path.GetFileName(Assembly.GetEntryAssembly()!.Location)} "); - return 1; + using (ConsoleCancellation(out var ct)) + { + return await Entry(args, ct); + } } - 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() + private static async Task Entry(string[] args, CancellationToken ct) { - ServiceProvider = serviceProvider, - Scheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler, - Endpoints = new() + 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) { - { typeof(IComputingService) }, - }, - Transport = serverTransport, - }; - ipcServer.Start(); - await Task.Delay(Timeout.InfiniteTimeSpan, ct); - - return 0; + var cts = new CancellationTokenSource(); + ct = cts.Token; + Console.CancelKeyPress += (sender, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + return cts; + } } -static IDisposable ConsoleCancellation(out CancellationToken ct) -{ - var cts = new CancellationTokenSource(); - ct = cts.Token; - Console.CancelKeyPress += (sender, e) => - { - e.Cancel = true; - cts.Cancel(); - }; - return cts; -} \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/RobotTests.cs b/src/UiPath.CoreIpc.Tests/RobotTests.cs index 29d6811a..bc173e51 100644 --- a/src/UiPath.CoreIpc.Tests/RobotTests.cs +++ b/src/UiPath.CoreIpc.Tests/RobotTests.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public abstract class RobotTests : TestBase { diff --git a/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs index 3cb2ad4b..95397289 100644 --- a/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/RobotTestsOverNamedPipes.cs @@ -4,7 +4,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class RobotTestsOverNamedPipes : RobotTests { @@ -113,9 +113,9 @@ public static TContract CreateProxy( new(actualKey, requestedParams, callbacks), CreateClient); - if (requestedParams != originalParams) + if (requestedParams - originalParams is { } ex) { - throw requestedParams - originalParams; + throw ex; } return client.GetProxy(); diff --git a/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs index 0e2f2e8c..4493a934 100644 --- a/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ArithmeticCallback.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class ArithmeticCallback : IArithmeticCallback { diff --git a/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs index 7c3df4b3..630a8c71 100644 --- a/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class ComputingCallback : IComputingCallback { diff --git a/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs index 9d174fc8..90d7994e 100644 --- a/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ComputingService.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class ComputingService(ILogger logger) : IComputingService { diff --git a/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs index d3ef2c34..8bfe8e00 100644 --- a/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/IComputingService.cs @@ -1,5 +1,5 @@  -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public interface IComputingServiceBase { diff --git a/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs index 93a096ba..8fd915ee 100644 --- a/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/ISystemService.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public interface ISystemService { diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs index 82068083..2469b06a 100644 --- a/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Contracts.cs @@ -1,4 +1,4 @@ -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public interface IStudioOperations : IStudioAgentOperations { diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs index 341385ba..f02fdea9 100644 --- a/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Impl.cs @@ -1,7 +1,7 @@ using Nito.AsyncEx; using Nito.Disposables; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public class StudioOperations : IStudioOperations { diff --git a/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs index 8f11e63b..8d832555 100644 --- a/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs +++ b/src/UiPath.CoreIpc.Tests/Services/Robot/Pals.cs @@ -1,7 +1,8 @@ using Nito.AsyncEx; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; internal sealed class Callbacks where T : class { @@ -11,11 +12,13 @@ internal sealed class Callbacks where T : class public bool TryRegister(Message message, out Callback callback) // false if already registered { - callback = _callbacks.FirstOrDefault(c => c.Client == message.Client); - if (callback != null) + 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"); diff --git a/src/UiPath.CoreIpc.Tests/Services/SystemService.cs b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs index 15299a24..0c96445f 100644 --- a/src/UiPath.CoreIpc.Tests/Services/SystemService.cs +++ b/src/UiPath.CoreIpc.Tests/Services/SystemService.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Text; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class SystemService : ISystemService { diff --git a/src/UiPath.CoreIpc.Tests/SpyTestBase.cs b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs index f52ea16d..2fc866f3 100644 --- a/src/UiPath.CoreIpc.Tests/SpyTestBase.cs +++ b/src/UiPath.CoreIpc.Tests/SpyTestBase.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public abstract class SpyTestBase : TestBase { diff --git a/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs index 1298d46a..6992eeeb 100644 --- a/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs +++ b/src/UiPath.CoreIpc.Tests/SyncOverAsyncTests.cs @@ -1,6 +1,6 @@ using UiPath.Ipc.Transport.NamedPipe; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public class SyncOverAsyncTests { diff --git a/src/UiPath.CoreIpc.Tests/SystemTests.cs b/src/UiPath.CoreIpc.Tests/SystemTests.cs index e00389df..70db74df 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTests.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTests.cs @@ -5,7 +5,7 @@ using System.Threading.Channels; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public abstract class SystemTests : TestBase { diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs index cb84c84a..27f83f70 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverNamedPipes.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.NamedPipe; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class SystemTestsOverNamedPipes : SystemTests { diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs index 161aa3e5..d9878b51 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverTcp.cs @@ -2,7 +2,7 @@ using UiPath.Ipc.Transport.Tcp; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class SystemTestsOverTcp : SystemTests { diff --git a/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs index f32bdf85..bc17f6f6 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTestsOverWebSockets.cs @@ -1,7 +1,7 @@ using UiPath.Ipc.Transport.WebSocket; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public sealed class SystemTestsOverWebSockets : SystemTests { diff --git a/src/UiPath.CoreIpc.Tests/TestBase.cs b/src/UiPath.CoreIpc.Tests/TestBase.cs index 4439f3ab..1318aeef 100644 --- a/src/UiPath.CoreIpc.Tests/TestBase.cs +++ b/src/UiPath.CoreIpc.Tests/TestBase.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using Xunit.Abstractions; -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public abstract class TestBase : IAsyncLifetime { diff --git a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj index 6b62feb9..53f000ee 100644 --- a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj +++ b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj @@ -3,12 +3,14 @@ WinExe net6.0;net461 + UiPath.Ipc.Tests UiPath.Ipc.Tests $(NoWarn);1998 $(DefineConstants);$(DefineConstantsEx) latest true enable + false diff --git a/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs index 32de313e..b954340b 100644 --- a/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs +++ b/src/UiPath.CoreIpc.Tests/Xunit/CustomTestFramework.cs @@ -3,7 +3,7 @@ [assembly: TestFramework(typeName: "UiPath.Ipc.Tests.CustomTestFramework", assemblyName: "UiPath.Ipc.Tests")] -namespace UiPath.CoreIpc.Tests; +namespace UiPath.Ipc.Tests; public readonly struct CustomTestContext { diff --git a/src/UiPath.CoreIpc/Helpers/Helpers.cs b/src/UiPath.CoreIpc/Helpers/Helpers.cs index 926cdb00..84eac97c 100644 --- a/src/UiPath.CoreIpc/Helpers/Helpers.cs +++ b/src/UiPath.CoreIpc/Helpers/Helpers.cs @@ -126,13 +126,14 @@ 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; } diff --git a/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs b/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs index f1f971a2..21bdd710 100644 --- a/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs +++ b/src/UiPath.CoreIpc/Helpers/TaskCompletionPool.cs @@ -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/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index 276985b1..64db7415 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -55,7 +55,7 @@ TCallbackInterface IClient.GetCallback() TCallbackInterface CreateCallback(Type callbackContract) { - _logger.LogInformation($"Create callback {callbackContract}."); + _logger?.LogInformation($"Create callback {callbackContract}."); return new ServiceClientForCallback(_connection, config: this).GetProxy(); } } From 5a52bb18a9a261ea1bad1233436827446ef53c29 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 10:04:52 +0200 Subject: [PATCH 18/26] fixes after review --- src/IpcSample.ConsoleClient/Client.cs | 2 +- ...iPath.CoreIpc.Extensions.Abstractions.csproj | 3 +-- ....CoreIpc.Extensions.BidirectionalHttp.csproj | 2 +- src/UiPath.CoreIpc/Config/IpcClient.cs | 15 --------------- src/UiPath.CoreIpc/Connection.cs | 17 +++++++++-------- src/UiPath.CoreIpc/Server/Server.cs | 2 +- src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs | 4 ++-- 7 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/IpcSample.ConsoleClient/Client.cs b/src/IpcSample.ConsoleClient/Client.cs index 2ab2cba6..92f03237 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -67,7 +67,7 @@ private static async Task RunTestsAsync(CancellationToken cancellationToken) 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}", cancellationToken); + Console.WriteLine($"[TEST 2] sum of 2 complex number is: {result3}"); // test 4: call IPC service method without parameter or return await systemClient.FireAndForgetWithCt(cancellationToken); diff --git a/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj index e1b2de66..4c768d09 100644 --- a/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj +++ b/src/UiPath.CoreIpc.Extensions.Abstractions/UiPath.CoreIpc.Extensions.Abstractions.csproj @@ -4,10 +4,9 @@ net6.0;net461;net6.0-windows enable enable - preview + latest true enable - true UiPath.Ipc.Extensions.Abstractions UiPath.Ipc.Extensions.Abstractions diff --git a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj index 8e55878f..9566e560 100644 --- a/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj +++ b/src/UiPath.CoreIpc.Extensions.BidirectionalHttp/UiPath.CoreIpc.Extensions.BidirectionalHttp.csproj @@ -4,7 +4,7 @@ net6.0;net461;net6.0-windows enable enable - preview + latest UiPath.Ipc.Extensions.BidirectionalHttp UiPath.Ipc.Extensions.BidirectionalHttp diff --git a/src/UiPath.CoreIpc/Config/IpcClient.cs b/src/UiPath.CoreIpc/Config/IpcClient.cs index 79ae4220..4e89198d 100644 --- a/src/UiPath.CoreIpc/Config/IpcClient.cs +++ b/src/UiPath.CoreIpc/Config/IpcClient.cs @@ -23,21 +23,6 @@ private ServiceClient GetServiceClient(Type proxyType) } public TProxy GetProxy() where TProxy : class => GetServiceClient(typeof(TProxy)).GetProxy(); - internal ILogger? GetLogger(string name) - { - if (Logger is not null) - { - return Logger; - } - - if (ServiceProvider?.GetService() is not { } loggerFactory) - { - return null; - } - - return loggerFactory.CreateLogger(name); - } - internal RouterConfig CreateCallbackRouterConfig() => RouterConfig.From( Callbacks.OrDefault(), diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index 3959f01e..1bd6d7bb 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -45,15 +45,15 @@ public Connection(Stream network, string debugName, ILogger? logger, int maxMess public override string ToString() => DebugName; public string NewRequestId() => Interlocked.Increment(ref _requestCounter).ToString(); - internal Task Listen() => _receiveLoop.Value; + public Task Listen() => _receiveLoop.Value; - internal event Func? RequestReceived; - internal event Action? CancellationReceived; + 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; @@ -98,7 +98,7 @@ internal async ValueTask RemoteCall(Request request, CancellationToken requestCompletion.Return(); } } - internal ValueTask Send(Request request, CancellationToken token) + public ValueTask Send(Request request, CancellationToken token) { Logger?.LogInformation("Connection.Send..."); var uploadStream = request.UploadStream; @@ -107,7 +107,7 @@ internal ValueTask Send(Request request, CancellationToken token) 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)) @@ -118,7 +118,7 @@ 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) { var responseBytes = SerializeToStream(response); return response.DownloadStream == null ? @@ -162,7 +162,8 @@ private async ValueTask SendStream(MessageType messageType, Stream data, Stream private async ValueTask SendMessage(MessageType messageType, MemoryStream data, CancellationToken cancellationToken) { Logger?.LogInformation("Connection.SendMessage: Awaiting the acquiring of the sendLock"); - await _sendLock.WaitAsync(cancellationToken); + await _sendLock.WaitAsync(cancellationToken); /// --------- + try { Logger?.LogInformation($"Connection.SendMessage: sendLock was successfully aquired. Pushing the bytes onto the network. ByteCount: {data.Length}"); diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index e49f8164..8fe18368 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -27,7 +27,7 @@ static Server() private readonly TimeSpan _requestTimeout; private ILogger? Logger => _connection.Logger; - private bool LogEnabled => Logger.Enabled(); + private bool LogEnabled => _connection.LogEnabled; public string DebugName => _connection.DebugName; public Server(Router router, TimeSpan requestTimeout, Connection connection, IClient? client = null) diff --git a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs index feb7a455..32f29140 100644 --- a/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs +++ b/src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs @@ -29,8 +29,8 @@ private void Serialize(object? obj, TextWriter streamWriter, JsonSerializer seri serializer.Serialize(writer, obj); writer.Flush(); } - public char[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); - public void Return(char[]? array) + char[] IArrayPool.Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); + void IArrayPool.Return(char[]? array) { if (array is null) { From a68ff893dce0ee09cf0d90110b60c3a04485f335 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 15:00:21 +0200 Subject: [PATCH 19/26] README.md --- README.md | 327 ++++++++++++++++++++++++++++++----- readme/diagram.svg | 3 + src/CoreIpc.sln | 1 + src/CoreIpc.sln.startup.json | 63 +++++++ 4 files changed, 353 insertions(+), 41 deletions(-) create mode 100644 readme/diagram.svg create mode 100644 src/CoreIpc.sln.startup.json diff --git a/README.md b/README.md index 587059e6..389d9996 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,286 @@ -[![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. | + +#### iii. `IpcServer` - 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. | + +#### iv. `IpcClient` - 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/CoreIpc.sln b/src/CoreIpc.sln index 297e2730..5c3bd64e 100644 --- a/src/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution 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}" 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": {} + } + } + } +} From b1db02ff0b5a2d6a53b56b4b40f80ff4cbe0833d Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 17:39:12 +0200 Subject: [PATCH 20/26] fixes after review --- NuGet.Config | 1 - README.md | 12 ++++++++---- src/CI/azp-dotnet.yaml | 2 +- .../Transport/WebSocket/WebSocketStream.cs | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 437e25e1..ba6d1755 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/README.md b/README.md index 389d9996..627b3a22 100644 --- a/README.md +++ b/README.md @@ -191,14 +191,16 @@ var ipcServer = new IpcServer | RequestTimeout | TimeSpan? | **Optional**, defaults to infinity: Interval after which the honoring of requests will time out. | -#### ii. `IpcServer` - declared properties +#### 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. | -#### iii. `IpcServer` - methods +**Methods** | Method | Description | | ------ | ----------- | @@ -208,14 +210,16 @@ var ipcServer = new IpcServer
-#### iii. `IpcClient` - declared properties +#### 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. | -#### iv. `IpcClient` - methods +**Methods** | Method | Notes | | ------ | ----- | diff --git a/src/CI/azp-dotnet.yaml b/src/CI/azp-dotnet.yaml index 515a8ea1..4a9c62a2 100644 --- a/src/CI/azp-dotnet.yaml +++ b/src/CI/azp-dotnet.yaml @@ -12,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/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs index eac709f9..91ef319e 100644 --- a/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs +++ b/src/UiPath.CoreIpc/Transport/WebSocket/WebSocketStream.cs @@ -5,7 +5,7 @@ namespace UiPath.Ipc.Transport.WebSocket; using WebSocket = System.Net.WebSockets.WebSocket; /// -/// Exposes a as a . +/// Exposes a as a . /// https://github.com/AArnott/Nerdbank.Streams/blob/main/src/Nerdbank.Streams/WebSocketStream.cs /// internal class WebSocketStream : Stream From 99150b2c54d09d550fe9342197201eaa56bb414f Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 17:48:55 +0200 Subject: [PATCH 21/26] fix arg order in EndpointNotFoundException --- src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs index de6ba869..238c06ea 100644 --- a/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs +++ b/src/UiPath.CoreIpc/Wire/EndpointNotFoundException.cs @@ -6,7 +6,7 @@ public sealed class EndpointNotFoundException : ArgumentException public string EndpointName { get; } internal EndpointNotFoundException(string paramName, string serverDebugName, string endpointName) - : base(paramName, FormatMessage(serverDebugName, endpointName)) + : base(FormatMessage(serverDebugName, endpointName), paramName) { ServerDebugName = serverDebugName; EndpointName = endpointName; From 26e731548ed5e34fef58c0f69ee283bc0a64ea8a Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 18:07:17 +0200 Subject: [PATCH 22/26] copilot suggestions --- src/UiPath.CoreIpc/Connection.cs | 4 ++-- src/UiPath.CoreIpc/report/generate-report.bat | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index 1bd6d7bb..df25299a 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -162,7 +162,7 @@ private async ValueTask SendStream(MessageType messageType, Stream data, Stream private async ValueTask SendMessage(MessageType messageType, MemoryStream data, CancellationToken cancellationToken) { Logger?.LogInformation("Connection.SendMessage: Awaiting the acquiring of the sendLock"); - await _sendLock.WaitAsync(cancellationToken); /// --------- + await _sendLock.WaitAsync(cancellationToken); try { @@ -407,7 +407,7 @@ private void OnResponseReceived(Response response) } } - private void Log(Exception ex) => Logger.LogException(ex, DebugName); + private void Log(Exception ex) => Logger?.LogException(ex, DebugName); private void Log(string message) { if (Logger is null) diff --git a/src/UiPath.CoreIpc/report/generate-report.bat b/src/UiPath.CoreIpc/report/generate-report.bat index 4efadd4f..9bceba9b 100644 --- a/src/UiPath.CoreIpc/report/generate-report.bat +++ b/src/UiPath.CoreIpc/report/generate-report.bat @@ -36,6 +36,6 @@ REM generate-public-api --target-frameworks "net6.0-windows" --assembly UiPath.I 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 "D:\Alt\coreipc\src\UiPath.CoreIpc\UiPath.CoreIpc.csproj" --output-directory "D:\Alt\coreipc\src\UiPath.CoreIpc\report" --verbose --leave-artifacts +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 From 1106f1de5a458ad9da99c0c344053e0e2be14298 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Mon, 14 Jul 2025 19:02:21 +0200 Subject: [PATCH 23/26] fix ubuntu --- src/CI/azp-start.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CI/azp-start.yaml b/src/CI/azp-start.yaml index d51e5497..ca321e0b 100644 --- a/src/CI/azp-start.yaml +++ b/src/CI/azp-start.yaml @@ -45,7 +45,7 @@ stages: - job: displayName: 'node.js on Ubuntu' pool: - vmImage: 'ubuntu-20.04' + vmImage: 'ubuntu-22.04' steps: - template: azp-initialization.yaml - template: azp-nodejs.yaml From a360574113b5e02a5a73bc4f68b53b83ed7cbfd8 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Wed, 16 Jul 2025 14:55:16 +0200 Subject: [PATCH 24/26] suppress logging of expected cancellation errors during server shutdown --- src/UiPath.CoreIpc/Config/IpcServer.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/UiPath.CoreIpc/Config/IpcServer.cs b/src/UiPath.CoreIpc/Config/IpcServer.cs index 74550865..dc94b5c1 100644 --- a/src/UiPath.CoreIpc/Config/IpcServer.cs +++ b/src/UiPath.CoreIpc/Config/IpcServer.cs @@ -132,22 +132,21 @@ private async Task DisposeCore() private async Task LoopAccept(CancellationToken ct) { - try + while (!ct.IsCancellationRequested) { - while (!ct.IsCancellationRequested) - { - await Accept(ct); - } - } - catch (OperationCanceledException ex) when (ex.CancellationToken == ct) - { - // Ignore + await TryAccept(ct); /// this method doesn't throw, and in case of non- exceptions, + /// it will notify the observer. } _newConnection.OnCompleted(); } - private async Task Accept(CancellationToken ct) + /// + /// 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(); @@ -159,11 +158,14 @@ private async Task Accept(CancellationToken ct) 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); - return; } } From 8e162440330fc5a8395c65ad120adea4cfb7cf57 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Thu, 17 Jul 2025 15:57:48 +0200 Subject: [PATCH 25/26] reduce log level for various log messages --- src/UiPath.CoreIpc/Connection.cs | 2 +- src/UiPath.CoreIpc/Logging/LoggingExtensions.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/UiPath.CoreIpc/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index df25299a..f401a759 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -67,7 +67,7 @@ public async ValueTask RemoteCall(Request request, CancellationToken t } catch (Exception ex) { - Logger?.LogError($"Caught exception while sending the request. Ex: {ex}"); + Logger?.LogTrace($"Caught exception while sending the request. Ex: {ex}"); tokenRegistration.Dispose(); if (_requests.TryRemove(requestId, out _)) { diff --git a/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs b/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs index 7de71a60..e4523187 100644 --- a/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs +++ b/src/UiPath.CoreIpc/Logging/LoggingExtensions.cs @@ -31,28 +31,28 @@ public enum Event [LoggerMessage( EventId = (int)Event.ServiceClient_Calling, EventName = nameof(Event.ServiceClient_Calling), - Level = LogLevel.Information, + 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.Information, + 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.Error, + 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.Information, + Level = LogLevel.Debug, Message = $$"""{{ServiceClient}} disposed. DebugName={debugName}.""")] public static partial void ServiceClient_Dispose(this ILogger logger, string debugName); @@ -66,7 +66,7 @@ public enum Event [LoggerMessage( EventId = (int)Event.Connection_ReceiveLoopEndedSuccessfully, EventName = nameof(Event.Connection_ReceiveLoopEndedSuccessfully), - Level = LogLevel.Information, + Level = LogLevel.Debug, Message = $$"""{{Connection}} receive loop ended successfully. DebugName={debugName}.""")] public static partial void Connection_ReceiveLoopEndedSuccessfully(this ILogger logger, string debugName); } From ed185818166379c185d911a162c8046e6181d1a3 Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Fri, 18 Jul 2025 10:50:45 +0200 Subject: [PATCH 26/26] reenable NuGet push constraint --- src/CI/azp-dotnet-dist.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CI/azp-dotnet-dist.yaml b/src/CI/azp-dotnet-dist.yaml index 02e66263..8cfffb63 100644 --- a/src/CI/azp-dotnet-dist.yaml +++ b/src/CI/azp-dotnet-dist.yaml @@ -24,7 +24,7 @@ steps: - task: PublishSymbols@2 displayName: 'Publish Symbols to UiPath Azure Artifacts Symbol Server' - condition: succeeded() + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) inputs: symbolsFolder: $(Build.SourcesDirectory) searchPattern: '**/UiPath.CoreIpc/bin/**/UiPath.CoreIpc.pdb'