From 2f10a01d9dee2bbf2bcd461c2b158e707c70939c Mon Sep 17 00:00:00 2001 From: Eduard Dumitru Date: Fri, 21 Jun 2024 13:54:59 +0200 Subject: [PATCH] everything new --- .editorconfig | 1 + .gitignore | 4 + CI/azp-dotnet-dist.yaml | 15 - CI/azp-dotnet.yaml | 15 - CI/azp-nodejs.yaml | 55 -- CI/azp-start.yaml | 50 -- Directory.Build.props | 7 - README.md | 14 +- src/CI/azp-dotnet-dist.yaml | 32 + src/CI/azp-dotnet.yaml | 9 + {CI => src/CI}/azp-initialization.yaml | 0 src/CI/azp-js.publish-npm.steps.yaml | 32 + {CI => src/CI}/azp-nodejs-dist.yaml | 4 +- src/CI/azp-nodejs.yaml | 73 ++ src/CI/azp-start.yaml | 66 ++ CoreIpc.sln => src/CoreIpc.sln | 13 +- src/IpcSample.ConsoleClient/Client.cs | 206 +++--- .../IpcSample.ConsoleClient.csproj | 12 +- src/IpcSample.ConsoleClient/TcpClient.cs | 220 +++--- .../WebSocketClient.cs | 133 ++++ .../IpcSample.ConsoleServer.csproj | 12 +- src/IpcSample.ConsoleServer/Server.cs | 85 ++- src/IpcSample.ConsoleServer/TcpServer.cs | 88 ++- .../WebSocketServer.cs | 49 ++ NuGet.Config => src/NuGet.Config | 0 src/UiPath.CoreIpc.Tests/ComputingTests.cs | 215 +++--- src/UiPath.CoreIpc.Tests/EndpointTests.cs | 242 ++++--- .../Implementation/ComputingCallback.cs | 29 +- .../Implementation/ComputingService.cs | 210 +++--- .../Implementation/IpcHelpers.cs | 105 +-- .../Implementation/OneWayStreamWrapper.cs | 145 ++-- .../Implementation/SystemService.cs | 274 ++++---- src/UiPath.CoreIpc.Tests/NamedPipeTests.cs | 99 ++- src/UiPath.CoreIpc.Tests/NestedStreamTests.cs | 624 +++++++++--------- src/UiPath.CoreIpc.Tests/SystemTests.cs | 480 +++++++------- src/UiPath.CoreIpc.Tests/TcpTests..cs | 49 +- src/UiPath.CoreIpc.Tests/TestBase.cs | 58 +- .../UiPath.CoreIpc.Tests.csproj | 25 +- src/UiPath.CoreIpc.Tests/ValidationTests.cs | 81 ++- src/UiPath.CoreIpc.Tests/WebSocketTests.cs | 49 ++ .../CancellationTokenSourcePool.cs | 50 ++ .../Client/ClientConnectionsRegistry.cs | 164 ++--- src/UiPath.CoreIpc/Client/ServiceClient.cs | 440 ++++++------ .../Client/ServiceClientBuilder.cs | 162 ++--- src/UiPath.CoreIpc/Connection.cs | 508 ++++++++------ src/UiPath.CoreIpc/Dtos.cs | 189 ++---- src/UiPath.CoreIpc/GlobalSuppressions.cs | 14 + src/UiPath.CoreIpc/Helpers.cs | 485 +++++++------- src/UiPath.CoreIpc/IpcJsonSerializer.cs | 76 ++- .../NamedPipe/NamedPipeClient.cs | 81 ++- .../NamedPipe/NamedPipeClientBuilder.cs | 88 ++- .../NamedPipe/NamedPipeListener.cs | 99 ++- src/UiPath.CoreIpc/NestedStream.cs | 252 +++---- src/UiPath.CoreIpc/Server/Listener.cs | 124 ++-- src/UiPath.CoreIpc/Server/Server.cs | 452 +++++++------ src/UiPath.CoreIpc/Server/ServerConnection.cs | 136 ++-- src/UiPath.CoreIpc/Server/ServiceHost.cs | 75 +-- .../Server/ServiceHostBuilder.cs | 124 ++-- src/UiPath.CoreIpc/TaskCompletionPool.cs | 27 + src/UiPath.CoreIpc/Tcp/TcpClient.cs | 68 +- src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs | 53 +- src/UiPath.CoreIpc/Tcp/TcpListener.cs | 78 +-- src/UiPath.CoreIpc/UiPath.CoreIpc.csproj | 41 +- .../WebSockets/WebSocketClient.cs | 38 ++ .../WebSockets/WebSocketClientBuilder.cs | 27 + .../WebSockets/WebSocketListener.cs | 24 + .../WebSockets/WebSocketStream.cs | 69 ++ 67 files changed, 4236 insertions(+), 3588 deletions(-) delete mode 100644 CI/azp-dotnet-dist.yaml delete mode 100644 CI/azp-dotnet.yaml delete mode 100644 CI/azp-nodejs.yaml delete mode 100644 CI/azp-start.yaml delete mode 100644 Directory.Build.props create mode 100644 src/CI/azp-dotnet-dist.yaml create mode 100644 src/CI/azp-dotnet.yaml rename {CI => src/CI}/azp-initialization.yaml (100%) create mode 100644 src/CI/azp-js.publish-npm.steps.yaml rename {CI => src/CI}/azp-nodejs-dist.yaml (69%) create mode 100644 src/CI/azp-nodejs.yaml create mode 100644 src/CI/azp-start.yaml rename CoreIpc.sln => src/CoreIpc.sln (81%) create mode 100644 src/IpcSample.ConsoleClient/WebSocketClient.cs create mode 100644 src/IpcSample.ConsoleServer/WebSocketServer.cs rename NuGet.Config => src/NuGet.Config (100%) create mode 100644 src/UiPath.CoreIpc.Tests/WebSocketTests.cs create mode 100644 src/UiPath.CoreIpc/CancellationTokenSourcePool.cs create mode 100644 src/UiPath.CoreIpc/GlobalSuppressions.cs create mode 100644 src/UiPath.CoreIpc/TaskCompletionPool.cs create mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs create mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs create mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs create mode 100644 src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs diff --git a/.editorconfig b/.editorconfig index d2698fa7..9027da3a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -58,6 +58,7 @@ dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local dotnet_naming_style.camel_case_style.capitalization = camel_case +csharp_style_pattern_local_over_anonymous_function=true:silent [*.xml] indent_size = 2 diff --git a/.gitignore b/.gitignore index e0dfee8e..fd4124b1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ scripts/ artifacts/ .dotnet TestResults/ +*.diagsession *.suo *.user bin @@ -32,3 +33,6 @@ project.lock.json # Read the Docs docs/_build /src/LastMajorVersionBinary + +# Mac Finder +.DS_Store diff --git a/CI/azp-dotnet-dist.yaml b/CI/azp-dotnet-dist.yaml deleted file mode 100644 index dbd6ff13..00000000 --- a/CI/azp-dotnet-dist.yaml +++ /dev/null @@ -1,15 +0,0 @@ -steps: - - task: CopyFiles@2 - displayName: '$(Label_DotNet) Copy nupkg to $(Build.ArtifactStagingDirectory)' - inputs: - SourceFolder: 'src\UiPath.CoreIpc\bin\$(DotNet_BuildConfiguration)\' - Contents: '*.*nupkg' - TargetFolder: '$(Build.ArtifactStagingDirectory)' - CleanTargetFolder: true - - - task: PublishBuildArtifacts@1 - displayName: '$(Label_DotNet) Publish the $(DotNet_ArtifactName) to the pipeline instance' - inputs: - ArtifactName: '$(DotNet_ArtifactName)' - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactType: 'Container' diff --git a/CI/azp-dotnet.yaml b/CI/azp-dotnet.yaml deleted file mode 100644 index a218a77e..00000000 --- a/CI/azp-dotnet.yaml +++ /dev/null @@ -1,15 +0,0 @@ -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: - command: 'test' - projects: '**/*Tests*.csproj' - publishTestResults: true - testRunTitle: '.NET tests' - arguments: '--no-build --configuration $(DotNet_BuildConfiguration) --logger "console;verbosity=detailed"' diff --git a/CI/azp-nodejs.yaml b/CI/azp-nodejs.yaml deleted file mode 100644 index 2b3fcbf1..00000000 --- a/CI/azp-nodejs.yaml +++ /dev/null @@ -1,55 +0,0 @@ -steps: - - task: DotNetCoreCLI@2 - displayName: '$(Label_NodeJS) Restore and build "UiPath.CoreIpc.csproj"' - inputs: - projects: '$(DotNet_MainProjectPath)' - arguments: '--configuration $(NodeJS_DotNet_BuildConfiguration) --framework netstandard2.0 -p:Version="$(FullVersion)"' - - - task: DotNetCoreCLI@2 - displayName: '$(Label_NodeJS) Restore and build "UiPath.CoreIpc.NodeInterop.csproj"' - inputs: - projects: '$(NodeJS_DotNetNodeInteropProject)' - arguments: '--configuration $(NodeJS_DotNet_BuildConfiguration) --framework net5.0 -p:Version="$(FullVersion)"' - - - task: Npm@1 - displayName: '$(Label_NodeJS) Write $[FullVersion] to package.json' - inputs: - command: 'custom' - workingDir: '$(NodeJS_ProjectPath)' - customCommand: 'version $(FullVersion) --allow-same-version' - - - task: Npm@1 - displayName: '$(Label_NodeJS) Restore' - inputs: - command: 'install' - workingDir: $(NodeJS_ProjectPath) - customRegistry: 'useFeed' - customFeed: '424ca518-1f12-456b-a4f6-888197fc15ee' - - - task: Npm@1 - displayName: '$(Label_NodeJS) Build' - inputs: - command: 'custom' - workingDir: $(NodeJS_ProjectPath) - customCommand: 'run build' - - - task: Npm@1 - displayName: '$(Label_NodeJS) Run unit tests' - inputs: - command: 'custom' - workingDir: $(NodeJS_ProjectPath) - customCommand: 'run test' - - - task: PublishTestResults@2 - displayName: '$(Label_NodeJS) Publish test results' - condition: succeededOrFailed() - inputs: - testRunner: JUnit - workingDir: $(NodeJS_ProjectPath) - testResultsFiles: './src/Clients/nodejs/reports/test-results.xml' - - - task: PublishCodeCoverageResults@1 - displayName: '$(Label_NodeJS) Publish code coverage results' - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: './src/Clients/nodejs/reports/coverage/cobertura-coverage.xml' diff --git a/CI/azp-start.yaml b/CI/azp-start.yaml deleted file mode 100644 index 47a12a74..00000000 --- a/CI/azp-start.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: $(Date:yyyyMMdd)$(Rev:-rr) - -variables: - Label_Initialization: 'Initialization:' - Label_DotNet: '.NET:' - Label_NodeJS: 'node.js:' - - DotNet_BuildConfiguration: 'Release' - DotNet_SessionSolution: 'CoreIpc.sln' - DotNet_MainProjectName: 'UiPath.CoreIpc' - DotNet_MainProjectPath: './src/UiPath.CoreIpc/UiPath.CoreIpc.csproj' - DotNet_ArtifactName: 'NuGet package' - - NodeJS_DotNet_BuildConfiguration: 'Debug' - NodeJS_ProjectPath: './src/Clients/nodejs' - NodeJS_ArchivePath: './src/Clients/nodejs.zip' - NodeJS_ArtifactName: 'NPM package' - NodeJS_NetCoreAppTargetDir_RelativePath: 'dotnet/UiPath.CoreIpc.NodeInterop/bin/Debug/net5.0' - NodeJS_DotNetNodeInteropProject : './src/Clients/nodejs/dotnet/UiPath.CoreIpc.NodeInterop/UiPath.CoreIpc.NodeInterop.csproj' - NodeJS_DotNetNodeInteropSolution: './src/Clients/nodejs/dotnet/UiPath.CoreIpc.NodeInterop.sln' - -jobs: - # The following 3 jobs will run in parallel: - - - job: - displayName: '.NET on Windows' - pool: - vmImage: 'windows-2019' - steps: - - template: azp-initialization.yaml - - template: azp-dotnet.yaml - - template: azp-dotnet-dist.yaml - - - job: - displayName: 'node.js on Windows' - pool: - vmImage: 'windows-2019' - 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 - diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 28c4f059..00000000 --- a/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - latest - $(DefineConstants);WINDOWS - $(DefineConstants);NET5_0_WINDOWS - - \ No newline at end of file diff --git a/README.md b/README.md index 5f6d2302..587059e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-MyGet-brightgreen)](https://www.myget.org/feed/uipath-dev/package/nuget/UiPath.CoreIpc) +[![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. .NET Standard (.NET Core) and [Node.js](src/Clients/nodejs) clients. +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 @@ -14,7 +14,7 @@ WCF-like service model API for communication over named pipes. .NET Standard (.N - configurable task scheduler - client authentication and impersonation - access to the underlying transport with `Stream` parameters -- SSPI encryption and signing +- SSL Check [the tests](https://github.com/UiPath/CoreIpc/blob/master/src/UiPath.CoreIpc.Tests/) and the sample. ```C# @@ -31,3 +31,11 @@ var computingClient = // 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 diff --git a/src/CI/azp-dotnet-dist.yaml b/src/CI/azp-dotnet-dist.yaml new file mode 100644 index 00000000..bafcc806 --- /dev/null +++ b/src/CI/azp-dotnet-dist.yaml @@ -0,0 +1,32 @@ +steps: + - task: CopyFiles@2 + displayName: '$(Label_DotNet) Copy nupkg to $(Build.ArtifactStagingDirectory)' + inputs: + SourceFolder: 'src\UiPath.CoreIpc\bin\$(DotNet_BuildConfiguration)\' + Contents: '*.*nupkg' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + CleanTargetFolder: true + + - task: PublishBuildArtifacts@1 + displayName: '$(Label_DotNet) Publish the $(DotNet_ArtifactName) to the pipeline instance' + inputs: + ArtifactName: '$(DotNet_ArtifactName)' + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactType: 'Container' + + - task: DotNetCoreCLI@2 + displayName: 'dotnet push to UiPath-Internal' + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + command: push + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' + publishVstsFeed: 'Public.Feeds/UiPath-Internal' + + - task: PublishSymbols@2 + displayName: 'Publish Symbols to UiPath Azure Artifacts Symbol Server' + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + symbolsFolder: $(Build.SourcesDirectory) + searchPattern: '**/UiPath.CoreIpc/bin/**/UiPath.CoreIpc.pdb' + symbolServerType: teamServices + indexSources: false \ No newline at end of file diff --git a/src/CI/azp-dotnet.yaml b/src/CI/azp-dotnet.yaml new file mode 100644 index 00000000..0af4fc84 --- /dev/null +++ b/src/CI/azp-dotnet.yaml @@ -0,0 +1,9 @@ +steps: + - task: DotNetCoreCLI@2 + displayName: '$(Label_DotNet) Run unit tests' + inputs: + command: 'test' + 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 diff --git a/CI/azp-initialization.yaml b/src/CI/azp-initialization.yaml similarity index 100% rename from CI/azp-initialization.yaml rename to src/CI/azp-initialization.yaml diff --git a/src/CI/azp-js.publish-npm.steps.yaml b/src/CI/azp-js.publish-npm.steps.yaml new file mode 100644 index 00000000..f03be252 --- /dev/null +++ b/src/CI/azp-js.publish-npm.steps.yaml @@ -0,0 +1,32 @@ +steps: +- checkout: none + +- download: current + artifact: 'NPM package' + # The destination path is $(Pipeline.Workspace) + +- task: NodeTool@0 + displayName: 'Use Node.js 20.11.0' + inputs: + versionSpec: '20.11.0' + +- task: ExtractFiles@1 + displayName: 'Extract Files' + inputs: + archiveFilePatterns: '$(Pipeline.Workspace)/NPM package/*.zip' + destinationFolder: '$(System.DefaultWorkingDirectory)/unzipped' + cleanDestinationFolder: true + +- task: Npm@1 + displayName: 'Publish NPM (NodeJS)' + inputs: + command: 'publish' + workingDir: '$(System.DefaultWorkingDirectory)/unzipped/dist/prepack/node' + publishEndpoint: PublishNPM + +- task: Npm@1 + displayName: 'Publish NPM (Web)' + inputs: + command: 'publish' + workingDir: '$(System.DefaultWorkingDirectory)/unzipped/dist/prepack/web' + publishEndpoint: PublishNPM diff --git a/CI/azp-nodejs-dist.yaml b/src/CI/azp-nodejs-dist.yaml similarity index 69% rename from CI/azp-nodejs-dist.yaml rename to src/CI/azp-nodejs-dist.yaml index d38d2c58..3002f2ea 100644 --- a/CI/azp-nodejs-dist.yaml +++ b/src/CI/azp-nodejs-dist.yaml @@ -1,6 +1,6 @@ steps: - task: ArchiveFiles@2 - displayName: '$(Label_NodeJS) Archive project directory' + displayName: 'Archive the project directory' inputs: archiveType: 'zip' includeRootFolder: false @@ -8,7 +8,7 @@ steps: archiveFile: '$(NodeJS_ArchivePath)' - task: PublishBuildArtifacts@1 - displayName: '$(Label_NodeJS) Publish the $(NodeJS_ArtifactName) to the pipeline instance' + displayName: 'Publish the $(NodeJS_ArtifactName) to the pipeline instance' inputs: ArtifactName: '$(NodeJS_ArtifactName)' PathtoPublish: '$(NodeJS_ArchivePath)' diff --git a/src/CI/azp-nodejs.yaml b/src/CI/azp-nodejs.yaml new file mode 100644 index 00000000..52f7d934 --- /dev/null +++ b/src/CI/azp-nodejs.yaml @@ -0,0 +1,73 @@ +steps: + - task: UseDotNet@2 + inputs: + version: 6.0.x + + - task: NodeTool@0 + displayName: 'Use Node.js 20.11.0' + inputs: + versionSpec: '20.11.0' + + - task: DotNetCoreCLI@2 + displayName: 'Build "UiPath.CoreIpc.csproj"' + inputs: + projects: '$(DotNet_MainProjectPath)' + arguments: '--configuration $(NodeJS_DotNet_BuildConfiguration) --framework net6.0 -p:Version="$(FullVersion)"' + + - task: DotNetCoreCLI@2 + displayName: 'Build "UiPath.CoreIpc.NodeInterop.csproj"' + inputs: + projects: '$(NodeJS_DotNetNodeInteropProject)' + arguments: '--configuration $(NodeJS_DotNet_BuildConfiguration) --framework net6.0 -p:Version="$(FullVersion)"' + + - task: CmdLine@2 + displayName: 'Set $[FullVersion] in package.json' + condition: succeeded() + inputs: + workingDirectory: '$(NodeJS_ProjectPath)' + script: 'npm version $(FullVersion) --allow-same-version' + + - task: Npm@1 + displayName: 'Npm Install' + inputs: + command: 'install' + workingDir: $(NodeJS_ProjectPath) + customRegistry: 'useFeed' + customFeed: '424ca518-1f12-456b-a4f6-888197fc15ee' + + - task: CmdLine@2 + displayName: 'Npm Run Build' + condition: succeeded() + inputs: + workingDirectory: $(NodeJS_ProjectPath) + script: 'npm run build' + + - task: CmdLine@2 + displayName: 'Npm Test' + condition: succeeded() + inputs: + workingDirectory: $(NodeJS_ProjectPath) + script: 'npm test' + + - task: PublishTestResults@2 + displayName: 'Publish Web Test Results' + condition: succeededOrFailed() + inputs: + testRunner: JUnit + workingDir: $(NodeJS_ProjectPath) + testResultsFiles: './src/Clients/js/reports/test/web/test-results.xml' + + - task: PublishTestResults@2 + displayName: 'Publish NodeJS Test Results' + condition: succeededOrFailed() + inputs: + testRunner: JUnit + workingDir: $(NodeJS_ProjectPath) + testRunTitle: '🌲 NodeJs ($(Agent.OS) $(Agent.OSArchitecture))' + testResultsFiles: './src/Clients/js/reports/test/node/test-results.xml' + + - task: PublishCodeCoverageResults@1 + displayName: 'Publish Code Coverage Results' + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: './src/Clients/js/reports/coverage/merged/cobertura/cobertura-coverage.xml' diff --git a/src/CI/azp-start.yaml b/src/CI/azp-start.yaml new file mode 100644 index 00000000..d659af93 --- /dev/null +++ b/src/CI/azp-start.yaml @@ -0,0 +1,66 @@ +name: $(Date:yyyyMMdd)$(Rev:-rr) + +variables: + Label_Initialization: 'Initialization:' + Label_DotNet: '.NET:' + Label_NodeJS: 'node.js:' + + DotNet_BuildConfiguration: 'Release' + DotNet_SessionSolution: './src/CoreIpc.sln' + 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' + NodeJS_ArtifactName: 'NPM package' + NodeJS_NetCoreAppTargetDir_RelativePath: 'dotnet/UiPath.CoreIpc.NodeInterop/bin/Debug/net6.0' + NodeJS_DotNetNodeInteropProject : './src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop/UiPath.CoreIpc.NodeInterop.csproj' + NodeJS_DotNetNodeInteropSolution: './src/Clients/js/dotnet/UiPath.CoreIpc.NodeInterop.sln' + +stages: +- stage: Build + displayName: '🏭 Build' + jobs: + # 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: + 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 + +- stage: Publish + displayName: 🚚 Publish + dependsOn: Build + jobs: + - deployment: Publish_NPM_Packages + displayName: '📦 Publish NPM Packages' + environment: 'NPM-Packages' + pool: + vmImage: ubuntu-latest + strategy: + runOnce: + deploy: + steps: + - template: azp-js.publish-npm.steps.yaml \ No newline at end of file diff --git a/CoreIpc.sln b/src/CoreIpc.sln similarity index 81% rename from CoreIpc.sln rename to src/CoreIpc.sln index fd30afaf..b69f1d5f 100644 --- a/CoreIpc.sln +++ b/src/CoreIpc.sln @@ -1,19 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.87 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31919.166 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IpcSample.ConsoleServer", "src\IpcSample.ConsoleServer\IpcSample.ConsoleServer.csproj", "{24A3C4D2-95A2-48D9-86F2-648879EC74F4}" +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", "src\IpcSample.ConsoleClient\IpcSample.ConsoleClient.csproj", "{8D54E62A-ECFF-4FFF-B9D1-DB343D456451}" +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", "src\UiPath.CoreIpc\UiPath.CoreIpc.csproj", "{58200319-1F71-4E22-894D-7E69E0CD0B57}" +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", "src\UiPath.CoreIpc.Tests\UiPath.CoreIpc.Tests.csproj", "{892424AE-4D3A-4984-914E-9423BE8D0212}" +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 - Directory.Build.props = Directory.Build.props NuGet.Config = NuGet.Config EndProjectSection EndProject diff --git a/src/IpcSample.ConsoleClient/Client.cs b/src/IpcSample.ConsoleClient/Client.cs index a884bcfa..36515be2 100644 --- a/src/IpcSample.ConsoleClient/Client.cs +++ b/src/IpcSample.ConsoleClient/Client.cs @@ -1,128 +1,124 @@ -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Text; using System.Diagnostics; using UiPath.CoreIpc.NamedPipe; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Linq; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +class Client { - class Client + static async Task Main(string[] args) { - static async Task _Main(string[] args) + Console.WriteLine(typeof(int).Assembly); + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + var source = new CancellationTokenSource(); + try { - Console.WriteLine(typeof(int).Assembly); - Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); - var source = new CancellationTokenSource(); - try + await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => { - await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => - { - Console.ReadLine(); - Console.WriteLine("Cancelling..."); - source.Cancel(); - })); - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); Console.ReadLine(); - } + Console.WriteLine("Cancelling..."); + source.Cancel(); + })); } - - private static async Task RunTestsAsync(CancellationToken cancellationToken) + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Console.ReadLine(); + } + } + private static async Task RunTestsAsync(CancellationToken cancellationToken) + { + var serviceProvider = ConfigureServices(); + var callback = new ComputingCallback { Id = "custom made" }; + var computingClientBuilder = new NamedPipeClientBuilder("test", serviceProvider) + .SerializeParametersAsObjects().CallbackInstance(callback).AllowImpersonation().RequestTimeout(TimeSpan.FromSeconds(2)); + var stopwatch = Stopwatch.StartNew(); + int count = 0; + try { - var serviceProvider = ConfigureServices(); - var callback = new ComputingCallback { Id = "custom made" }; - var computingClientBuilder = new NamedPipeClientBuilder("test", serviceProvider).CallbackInstance(callback).AllowImpersonation().RequestTimeout(TimeSpan.FromSeconds(2)); - var stopwatch = Stopwatch.StartNew(); - int count = 0; - try + var computingClient = computingClientBuilder.ValidateAndBuild(); + var systemClient = + new NamedPipeClientBuilder("test") + .SerializeParametersAsObjects() + .RequestTimeout(TimeSpan.FromSeconds(2)) + .Logger(serviceProvider) + .AllowImpersonation() + .ValidateAndBuild(); + for (int i = 0; i < int.MaxValue; i++) { - var computingClient = computingClientBuilder.ValidateAndBuild(); - var systemClient = - new NamedPipeClientBuilder("test") - .RequestTimeout(TimeSpan.FromSeconds(2)) - .Logger(serviceProvider) - .AllowImpersonation() - .ValidateAndBuild(); - while (true) - { - // test 1: call IPC service method with primitive types - float result1 = await computingClient.AddFloat(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"); + // test 1: call IPC service method with primitive types + float result1 = await computingClient.AddFloat(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"); - // 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); + // 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); - // test 4: call IPC service method without parameter or return - await systemClient.DoNothing(cancellationToken); - Console.WriteLine($"[TEST 4] invoked DoNothing()"); - //((IDisposable)systemClient).Dispose(); + // test 4: call IPC service method without parameter or return + await systemClient.DoNothing(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); - Console.WriteLine($"[TEST 5] {text}"); + // test 5: call IPC service method with enum parameter + string text = await systemClient.ConvertText("hEllO woRd!", TextStyle.Upper, cancellationToken); + Console.WriteLine($"[TEST 5] {text}"); - // test 6: call IPC service method returning GUID - Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), cancellationToken); - Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); + // test 6: call IPC service method returning GUID + Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), 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[] reversed = await systemClient.ReverseBytes(input, cancellationToken); - Console.WriteLine($"[TEST 7] reverse bytes"); + // test 7: call IPC service method with byte array + byte[] input = Encoding.UTF8.GetBytes(string.Concat(Enumerable.Range(1, 1).Select(_ => "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); - Console.WriteLine($"[TEST 8] client identity : {userName}"); + // test 8: call IPC service method with callback + var userName = await computingClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + Console.WriteLine($"[TEST 8] client identity : {userName}"); - //// test 9: call IPC service method with message parameter - ////Console.WriteLine($"[TEST 9] callback error"); - //try - //{ - // //userName = await systemClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); - //} - //catch(Exception ex) - //{ - // //Console.WriteLineex.Message); - //} - } - var callbackProxy = (IDisposable)computingClient; - callbackProxy.Dispose(); - callbackProxy.Dispose(); - callbackProxy.Dispose(); - } - finally - { - stopwatch.Stop(); - Console.WriteLine(); - Console.WriteLine("Calls per second: " + count / stopwatch.Elapsed.TotalSeconds); - Console.WriteLine(); + //// test 9: call IPC service method with message parameter + ////Console.WriteLine($"[TEST 9] callback error"); + //try + //{ + // //userName = await systemClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + //} + //catch(Exception ex) + //{ + // //Console.WriteLineex.Message); + //} } - // test 10: call slow IPC service method - //await systemClient.SlowOperation(cancellationToken); - //Console.WriteLine($"[TEST 10] Called slow operation"); + stopwatch.Stop(); + var callbackProxy = (IDisposable)computingClient; + callbackProxy.Dispose(); + callbackProxy.Dispose(); + callbackProxy.Dispose(); } - - private static IServiceProvider ConfigureServices() => - new ServiceCollection() - .AddIpcWithLogging() - .BuildServiceProvider(); + finally + { + stopwatch.Stop(); + Console.WriteLine(); + Console.WriteLine("Calls per second: " + count*8 / stopwatch.Elapsed.TotalSeconds); + Console.WriteLine(); + } + // test 10: call slow IPC service method + //await systemClient.SlowOperation(cancellationToken); + //Console.WriteLine($"[TEST 10] Called slow operation"); } + + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj index a79c7914..a313c304 100644 --- a/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj +++ b/src/IpcSample.ConsoleClient/IpcSample.ConsoleClient.csproj @@ -2,8 +2,10 @@ Exe - net5.0;net461;net5.0-windows + net7.0;net461;net7.0-windows app1.manifest + latest + true @@ -12,9 +14,9 @@ - - - - + + + + diff --git a/src/IpcSample.ConsoleClient/TcpClient.cs b/src/IpcSample.ConsoleClient/TcpClient.cs index 1e00b4fd..0768ebc6 100644 --- a/src/IpcSample.ConsoleClient/TcpClient.cs +++ b/src/IpcSample.ConsoleClient/TcpClient.cs @@ -1,134 +1,136 @@ -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Text; using System.Diagnostics; using UiPath.CoreIpc.Tcp; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Linq; using System.Net; -using System.IO; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +class TcpClient { - class TcpClient + static readonly IPEndPoint SystemEndPoint = new(IPAddress.Loopback, 3131); + static async Task _Main(string[] args) { - static readonly IPEndPoint SystemEndPoint = new(IPAddress.Loopback, 3131); - static async Task Main(string[] args) + Console.WriteLine(typeof(int).Assembly); + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + var source = new CancellationTokenSource(); + try { - Console.WriteLine(typeof(int).Assembly); - Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); - var source = new CancellationTokenSource(); - try + await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => { - await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => - { - Console.ReadLine(); - Console.WriteLine("Cancelling..."); - source.Cancel(); - })); - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); Console.ReadLine(); - } + Console.WriteLine("Cancelling..."); + source.Cancel(); + })); } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Console.ReadLine(); + } + } - private static async Task RunTestsAsync(CancellationToken cancellationToken) + private static async Task RunTestsAsync(CancellationToken cancellationToken) + { + var serviceProvider = ConfigureServices(); + var callback = new ComputingCallback { Id = "custom made" }; + var computingClientBuilder = new TcpClientBuilder(SystemEndPoint, serviceProvider) + .SerializeParametersAsObjects().CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); + var stopwatch = Stopwatch.StartNew(); + int count = 0; + try { - var serviceProvider = ConfigureServices(); - var callback = new ComputingCallback { Id = "custom made" }; - var computingClientBuilder = new TcpClientBuilder(SystemEndPoint, serviceProvider).CallbackInstance(callback).RequestTimeout(TimeSpan.FromSeconds(2)); - var stopwatch = Stopwatch.StartNew(); - int count = 0; - try + var computingClient = computingClientBuilder.ValidateAndBuild(); + var systemClient = + new TcpClientBuilder(SystemEndPoint) + .SerializeParametersAsObjects() + //.EncryptAndSign("localhost") + .RequestTimeout(TimeSpan.FromSeconds(2)) + .Logger(serviceProvider) + .ValidateAndBuild(); + var 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++) { - var computingClient = computingClientBuilder.ValidateAndBuild(); - var systemClient = - new TcpClientBuilder(SystemEndPoint) - .RequestTimeout(TimeSpan.FromSeconds(2)) - .Logger(serviceProvider) - .ValidateAndBuild(); - using (var file = File.OpenRead(@"C:\Windows\DPINST.log")) - { - Console.WriteLine(await systemClient.Upload(file)); - } - { - // test 1: call IPC service method with primitive types - float result1 = await computingClient.AddFloat(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"); + // test 1: call IPC service method with primitive types + float result1 = await computingClient.AddFloat(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"); - // 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); + // 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); - // test 4: call IPC service method without parameter or return - await systemClient.DoNothing(cancellationToken); - Console.WriteLine($"[TEST 4] invoked DoNothing()"); - //((IDisposable)systemClient).Dispose(); + // test 4: call IPC service method without parameter or return + //await systemClient.DoNothing(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); - Console.WriteLine($"[TEST 5] {text}"); + // test 5: call IPC service method with enum parameter + string text = await systemClient.ConvertText("hEllO woRd!", TextStyle.Upper, cancellationToken); + Console.WriteLine($"[TEST 5] {text}"); - // test 6: call IPC service method returning GUID - Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), cancellationToken); - Console.WriteLine($"[TEST 6] generated ID is: {generatedId}"); + // test 6: call IPC service method returning GUID + Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), 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[] reversed = await systemClient.ReverseBytes(input, cancellationToken); - Console.WriteLine($"[TEST 7] reverse bytes"); + // test 7: call IPC service method with byte array + byte[] input = Encoding.UTF8.GetBytes(string.Concat(Enumerable.Range(1, 1).Select(_ => "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); - Console.WriteLine($"[TEST 8] client identity : {userName}"); + // test 8: call IPC service method with callback + var userName = await computingClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + Console.WriteLine($"[TEST 8] client identity : {userName}"); - //// test 9: call IPC service method with message parameter - ////Console.WriteLine($"[TEST 9] callback error"); - //try - //{ - // //userName = await systemClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); - //} - //catch(Exception ex) - //{ - // //Console.WriteLineex.Message); - //} - } - var callbackProxy = (IDisposable)computingClient; - callbackProxy.Dispose(); - callbackProxy.Dispose(); - callbackProxy.Dispose(); - //((IpcProxy)callbackProxy).CloseConnection(); - } - finally - { - stopwatch.Stop(); - Console.WriteLine(); - Console.WriteLine("Calls per second: " + count / stopwatch.Elapsed.TotalSeconds); - Console.WriteLine(); + //// test 9: call IPC service method with message parameter + ////Console.WriteLine($"[TEST 9] callback error"); + //try + //{ + // //userName = await systemClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + //} + //catch(Exception ex) + //{ + // //Console.WriteLineex.Message); + //} } - // test 10: call slow IPC service method - //await systemClient.SlowOperation(cancellationToken); - //Console.WriteLine($"[TEST 10] Called slow operation"); + watch.Stop(); + Console.WriteLine(watch.ElapsedMilliseconds); + var callbackProxy = (IDisposable)computingClient; + callbackProxy.Dispose(); + callbackProxy.Dispose(); + callbackProxy.Dispose(); + //((IpcProxy)callbackProxy).CloseConnection(); + ((IpcProxy)computingClient).CloseConnection(); + ((IpcProxy)systemClient).CloseConnection(); } - - private static IServiceProvider ConfigureServices() => - new ServiceCollection() - .AddIpcWithLogging() - .BuildServiceProvider(); + finally + { + stopwatch.Stop(); + Console.WriteLine(); + Console.WriteLine("Calls per second: " + count / stopwatch.Elapsed.TotalSeconds); + Console.WriteLine(); + } + // test 10: call slow IPC service method + //await systemClient.SlowOperation(cancellationToken); + //Console.WriteLine($"[TEST 10] Called slow operation"); } + + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleClient/WebSocketClient.cs b/src/IpcSample.ConsoleClient/WebSocketClient.cs new file mode 100644 index 00000000..e6c93422 --- /dev/null +++ b/src/IpcSample.ConsoleClient/WebSocketClient.cs @@ -0,0 +1,133 @@ +using System.Text; +using System.Diagnostics; +using UiPath.CoreIpc.WebSockets; +using Microsoft.Extensions.DependencyInjection; +namespace UiPath.CoreIpc.Tests; +class WebSocketClient +{ + static async Task _Main(string[] args) + { + Console.WriteLine(typeof(int).Assembly); + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + Thread.Sleep(1000); + var source = new CancellationTokenSource(); + try + { + await await Task.WhenAny(RunTestsAsync(source.Token), Task.Run(() => + { + Console.ReadLine(); + Console.WriteLine("Cancelling..."); + source.Cancel(); + })); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Console.ReadLine(); + } + } + + private static async Task RunTestsAsync(CancellationToken cancellationToken) + { + Uri uri = new("ws://localhost:1212/wsDemo/"); + var serviceProvider = ConfigureServices(); + var callback = new ComputingCallback { Id = "custom made" }; + var computingClientBuilder = new WebSocketClientBuilder(uri, serviceProvider).SerializeParametersAsObjects() + .CallbackInstance(callback)/*.EncryptAndSign("localhost")*/.RequestTimeout(TimeSpan.FromSeconds(2)); + var stopwatch = Stopwatch.StartNew(); + int count = 0; + try + { + var computingClient = computingClientBuilder.ValidateAndBuild(); + var systemClient = + new WebSocketClientBuilder(uri).SerializeParametersAsObjects() + //.EncryptAndSign("localhost") + .RequestTimeout(TimeSpan.FromSeconds(2)) + .Logger(serviceProvider) + .ValidateAndBuild(); + var 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++) + { + // test 1: call IPC service method with primitive types + float result1 = await computingClient.AddFloat(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"); + + // 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); + + // test 4: call IPC service method without parameter or return + //await systemClient.DoNothing(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); + Console.WriteLine($"[TEST 5] {text}"); + + // test 6: call IPC service method returning GUID + Guid generatedId = await systemClient.GetGuid(Guid.NewGuid(), 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[] 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); + Console.WriteLine($"[TEST 8] client identity : {userName}"); + + //// test 9: call IPC service method with message parameter + ////Console.WriteLine($"[TEST 9] callback error"); + //try + //{ + // //userName = await systemClient.SendMessage(new SystemMessage { Text = "client text" }, cancellationToken); + //} + //catch(Exception ex) + //{ + // //Console.WriteLineex.Message); + //} + } + watch.Stop(); + Console.WriteLine(watch.ElapsedMilliseconds); + var callbackProxy = (IDisposable)computingClient; + callbackProxy.Dispose(); + callbackProxy.Dispose(); + callbackProxy.Dispose(); + //((IpcProxy)callbackProxy).CloseConnection(); + ((IpcProxy)computingClient).CloseConnection(); + ((IpcProxy)systemClient).CloseConnection(); + } + finally + { + stopwatch.Stop(); + Console.WriteLine(); + Console.WriteLine("Calls per second: " + count / stopwatch.Elapsed.TotalSeconds); + Console.WriteLine(); + } + // test 10: call slow IPC service method + //await systemClient.SlowOperation(cancellationToken); + //Console.WriteLine($"[TEST 10] Called slow operation"); + } + + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .BuildServiceProvider(); +} \ No newline at end of file diff --git a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj index 3020f96c..2c9744b6 100644 --- a/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj +++ b/src/IpcSample.ConsoleServer/IpcSample.ConsoleServer.csproj @@ -2,8 +2,10 @@ Exe - net5.0;net461;net5.0-windows + net7.0;net461;net7.0-windows app1.manifest + latest + true @@ -12,10 +14,10 @@ - - - - + + + + diff --git a/src/IpcSample.ConsoleServer/Server.cs b/src/IpcSample.ConsoleServer/Server.cs index 5e83849f..8e9ad9e4 100644 --- a/src/IpcSample.ConsoleServer/Server.cs +++ b/src/IpcSample.ConsoleServer/Server.cs @@ -1,58 +1,49 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; using System.Diagnostics; -using System.IO.Pipes; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; using UiPath.CoreIpc.NamedPipe; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +class Server { - 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() { - //private static readonly Timer _timer = new Timer(_ => - //{ - // Console.WriteLine("GC.Collect"); - // GC.Collect(); - // GC.WaitForPendingFinalizers(); - // GC.Collect(); - //}, null, 0, 3000); - - static async Task _Main() - { - Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); - //GuiLikeSyncContext.Install(); - Console.WriteLine(SynchronizationContext.Current); - var serviceProvider = ConfigureServices(); - // build and run service host - var host = new ServiceHostBuilder(serviceProvider) - .UseNamedPipes(new NamedPipeSettings("test") - { - RequestTimeout = TimeSpan.FromSeconds(2), - AccessControl = security => security.AllowCurrentUser(), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); - - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + //GuiLikeSyncContext.Install(); + Console.WriteLine(SynchronizationContext.Current); + var serviceProvider = ConfigureServices(); + // build and run service host + var host = new ServiceHostBuilder(serviceProvider) + .UseNamedPipes(new NamedPipeSettings("test") { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - host.Dispose(); - })); + RequestTimeout = TimeSpan.FromSeconds(2), + //AccessControl = security => security.AllowCurrentUser(), + }) + .AddEndpoint() + .AddEndpoint() + .ValidateAndBuild(); - Console.WriteLine("Server stopped."); - } + await await Task.WhenAny(host.RunAsync(), Task.Run(() => + { + Console.WriteLine(typeof(int).Assembly); + Console.ReadLine(); + host.Dispose(); + })); - private static IServiceProvider ConfigureServices() => - new ServiceCollection() - .AddIpcWithLogging() - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); + Console.WriteLine("Server stopped."); } + + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleServer/TcpServer.cs b/src/IpcSample.ConsoleServer/TcpServer.cs index b52042a7..172a7725 100644 --- a/src/IpcSample.ConsoleServer/TcpServer.cs +++ b/src/IpcSample.ConsoleServer/TcpServer.cs @@ -1,59 +1,53 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; using System.Diagnostics; -using System.IO.Pipes; using System.Net; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; using UiPath.CoreIpc.Tcp; -namespace UiPath.CoreIpc.Tests -{ - 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); +namespace UiPath.CoreIpc.Tests; - static async Task Main() - { - Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); - //GuiLikeSyncContext.Install(); - Console.WriteLine(SynchronizationContext.Current); - var serviceProvider = ConfigureServices(); - // build and run service host - var host = new ServiceHostBuilder(serviceProvider) - .UseTcp(new TcpSettings(SystemEndPoint) - { - RequestTimeout = TimeSpan.FromSeconds(2), - }) - .AddEndpoint() - .AddEndpoint() - .ValidateAndBuild(); +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); - await await Task.WhenAny(host.RunAsync(), Task.Run(() => + static async Task _Main() + { + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + //GuiLikeSyncContext.Install(); + Console.WriteLine(SynchronizationContext.Current); + var serviceProvider = ConfigureServices(); + // build and run service host + var data = File.ReadAllBytes(@"../../../../localhost.pfx"); + var host = new ServiceHostBuilder(serviceProvider) + .UseTcp(new TcpSettings(SystemEndPoint) { - Console.WriteLine(typeof(int).Assembly); - Console.ReadLine(); - host.Dispose(); - })); + RequestTimeout = TimeSpan.FromSeconds(2), + //Certificate = new X509Certificate(data, "1"), + }) + .AddEndpoint() + .AddEndpoint() + .ValidateAndBuild(); - Console.WriteLine("Server stopped."); - } + await await Task.WhenAny(host.RunAsync(), Task.Run(() => + { + Console.WriteLine(typeof(int).Assembly); + Console.ReadLine(); + host.Dispose(); + })); - private static IServiceProvider ConfigureServices() => - new ServiceCollection() - .AddIpcWithLogging() - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); + Console.WriteLine("Server stopped."); } + + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); } \ No newline at end of file diff --git a/src/IpcSample.ConsoleServer/WebSocketServer.cs b/src/IpcSample.ConsoleServer/WebSocketServer.cs new file mode 100644 index 00000000..322c1d65 --- /dev/null +++ b/src/IpcSample.ConsoleServer/WebSocketServer.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Net; +using System.Net.WebSockets; +using UiPath.CoreIpc.WebSockets; +namespace UiPath.CoreIpc.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() + { + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + //GuiLikeSyncContext.Install(); + Console.WriteLine(SynchronizationContext.Current); + var serviceProvider = ConfigureServices(); + // build and run service host + //var data = File.ReadAllBytes(@"../../../../localhost.pfx"); + var host = new ServiceHostBuilder(serviceProvider) + .UseWebSockets(new(new HttpSysWebSocketsListener("http://localhost:1212/wsDemo/").Accept) + { + RequestTimeout = TimeSpan.FromSeconds(2), + //Certificate = new X509Certificate(data, "1"), + }) + .AddEndpoint() + .AddEndpoint() + .ValidateAndBuild(); + await await Task.WhenAny(host.RunAsync(), Task.Run(() => + { + Console.WriteLine(typeof(int).Assembly); + Console.ReadLine(); + host.Dispose(); + })); + Console.WriteLine("Server stopped."); + return; + } + private static IServiceProvider ConfigureServices() => + new ServiceCollection() + .AddIpcWithLogging() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); +} \ No newline at end of file diff --git a/NuGet.Config b/src/NuGet.Config similarity index 100% rename from NuGet.Config rename to src/NuGet.Config diff --git a/src/UiPath.CoreIpc.Tests/ComputingTests.cs b/src/UiPath.CoreIpc.Tests/ComputingTests.cs index 10f4f8be..6c562dd9 100644 --- a/src/UiPath.CoreIpc.Tests/ComputingTests.cs +++ b/src/UiPath.CoreIpc.Tests/ComputingTests.cs @@ -1,138 +1,129 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Shouldly; -using Xunit; +namespace UiPath.CoreIpc.Tests; -namespace UiPath.CoreIpc.Tests +public abstract class ComputingTests : TestBase where TBuilder : ServiceClientBuilder { - 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() { - protected readonly ServiceHost _computingHost; - protected readonly IComputingService _computingClient; - protected readonly ComputingService _computingService; - protected readonly ComputingCallback _computingCallback; - public ComputingTests() - { - _computingCallback = new ComputingCallback { Id = System.Guid.NewGuid().ToString() }; - _computingService = (ComputingService)_serviceProvider.GetService(); - _computingHost = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint() - .ValidateAndBuild(); - _computingHost.RunAsync(GuiScheduler); - _computingClient = ComputingClientBuilder(GuiScheduler).ValidateAndBuild(); - } - protected abstract TBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null); - [Fact] - public async Task ReconnectWithEncrypt() + _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++) { - for (int i = 0; i < 50; i++) - { - await _computingClient.AddFloat(1, 2); - ((IpcProxy)_computingClient).CloseConnection(); - await _computingClient.AddFloat(1, 2); - } + 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 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 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 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() + [Fact] + public async Task ClientCancellation() + { + using (var cancellationSource = new CancellationTokenSource(10)) { - using (var cancellationSource = new CancellationTokenSource(10)) - { - _computingClient.Infinite(cancellationSource.Token).ShouldThrow(); - } - await AddFloat(); + _computingClient.Infinite(cancellationSource.Token).ShouldThrow(); } + await AddFloat(); + } - [Fact] - public async Task ClientTimeout() - { - var proxy = ComputingClientBuilder().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 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() + [Fact] + public async Task TimeoutPerRequest() + { + for (int i = 0; i < 20; i++) { - 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) { - var request = new SystemMessage { RequestTimeout = TimeSpan.FromTicks(1), 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(); + 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 Task InfiniteVoid() => _computingClient.InfiniteVoid(); - [Fact] - public async Task AddComplexNumbers() + [Fact] + public async Task AddComplexNumbers() + { + var result = await _computingClient.AddComplexNumbers(new[] { - 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)); - } + 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 async Task GetCallbackThreadName() => (await _computingClient.GetCallbackThreadName()).ShouldBe("GuiThread"); - [Fact] - public Task CallbackConcurrently() => Task.WhenAll(Enumerable.Range(1, 50).Select(_ => Callback())); + [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}"); - } + [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(); - } + 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 index 65eeba18..e62db908 100644 --- a/src/UiPath.CoreIpc.Tests/EndpointTests.cs +++ b/src/UiPath.CoreIpc.Tests/EndpointTests.cs @@ -1,145 +1,141 @@ -using System; -using System.Threading; -using System.Linq; -using System.Threading.Tasks; -using UiPath.CoreIpc.NamedPipe; -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Shouldly; +namespace UiPath.CoreIpc.Tests; -namespace UiPath.CoreIpc.Tests +public class EndpointTests : IDisposable { - 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() { - 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) - .TaskScheduler(taskScheduler); - private ISystemService CreateSystemService() => SystemClientBuilder().ValidateAndBuild(); - private NamedPipeClientBuilder SystemClientBuilder() => - new NamedPipeClientBuilder(PipeName, _serviceProvider).CallbackInstance(_systemCallback).RequestTimeout(RequestTimeout).AllowImpersonation(); - public void Dispose() + _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++) { - ((IDisposable)_computingClient).Dispose(); - ((IDisposable)_systemClient).Dispose(); + await CallbackCore(); ((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() + 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 { - var proxy = new NamedPipeClientBuilder(PipeName).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); + await _systemClient.MissingCallback(new SystemMessage()); } - - [Fact] - public async Task MissingCallback() + catch (RemoteException ex) { - 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(); + exception = ex; } - [Fact] - public Task CancelServerCall() => CancelServerCallCore(10); + 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) + async Task CancelServerCallCore(int counter) + { + for (int i = 0; i < counter; i++) { - for (int i = 0; i < counter; i++) + var request = new SystemMessage { RequestTimeout = Timeout.InfiniteTimeSpan, Delay = Timeout.Infinite }; + Task sendMessageResult; + using (var cancellationSource = new CancellationTokenSource()) { - 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(); + 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); } - } - - [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."); + ((IDisposable)_systemClient).Dispose(); } } - public interface ISystemCallback + + [Fact] + public async Task DuplicateCallbackProxies() { - Task GetId(Message message = null); + 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 class SystemCallback : ISystemCallback +} +public interface ISystemCallback +{ + Task GetId(Message message = null); +} +public class SystemCallback : ISystemCallback +{ + public string Id { get; set; } + public async Task GetId(Message message) { - public string Id { get; set; } - public async Task GetId(Message message) - { - message.Client.ShouldBeNull(); - return Id; - } + 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 index 169adbac..7938f812 100644 --- a/src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs +++ b/src/UiPath.CoreIpc.Tests/Implementation/ComputingCallback.cs @@ -1,23 +1,18 @@ -using Shouldly; -using System.Threading; -using System.Threading.Tasks; +namespace UiPath.CoreIpc.Tests; -namespace UiPath.CoreIpc.Tests +public interface IComputingCallback { - public interface IComputingCallback + Task GetId(Message message); + Task GetThreadName(); +} +public class ComputingCallback : IComputingCallback +{ + public string Id { get; set; } + public async Task GetId(Message message) { - Task GetId(Message message); - Task GetThreadName(); + message.Client.ShouldBeNull(); + return Id; } - 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; - } + 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 index bec2febf..908ee1f0 100644 --- a/src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs +++ b/src/UiPath.CoreIpc.Tests/Implementation/ComputingService.cs @@ -1,134 +1,136 @@ using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +public interface IInvalid : IDisposable { - public interface IInvalid : IDisposable - { - } +} - public interface IDuplicateMessage - { - Task Test(Message message1, Message message2); - } +public interface IDuplicateMessage +{ + Task Test(Message message1, Message message2); +} - public interface IUploadNotification - { - Task Upload(Stream stream); - } +public interface IUploadNotification +{ + Task Upload(Stream stream); +} - public interface IDerivedStreamDownload - { - Task Download(); - } +public interface IDerivedStreamDownload +{ + Task Download(); +} - public interface IDuplicateStreams - { - Task Upload(Stream stream, Stream stream2); - } +public interface IDuplicateStreams +{ + Task Upload(Stream stream, Stream stream2); +} - public interface IDerivedStreamUpload - { - Task Upload(MemoryStream stream); - } +public interface IDerivedStreamUpload +{ + Task Upload(MemoryStream stream); +} - public interface IMessageFirst - { - Task Test(Message message1, int x); - } +public interface IMessageFirst +{ + Task Test(Message message1, int x); +} + +public interface IInvalidCancellationToken +{ + Task Test(CancellationToken token, int x); +} - public interface IInvalidCancellationToken +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) { - Task Test(CancellationToken token, int x); + A = a; + B = b; } +} + +public enum TextStyle +{ + TitleCase, + Upper +} - public interface IComputingServiceBase +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 { - Task AddFloat(float x, float y, CancellationToken cancellationToken = default); + _logger = logger; } - public interface IComputingService : IComputingServiceBase + + public async Task AddComplexNumber(ComplexNumber x, ComplexNumber y, CancellationToken cancellationToken = default) { - 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); + _logger.LogInformation($"{nameof(AddComplexNumber)} called."); + return new ComplexNumber(x.A + y.A, x.B + y.B); } - public struct ComplexNumber + public async Task AddComplexNumbers(IEnumerable numbers, CancellationToken cancellationToken = default) { - public float A { get; set; } - public float B { get; set; } - - public ComplexNumber(float a, float b) + _logger.LogInformation($"{nameof(AddComplexNumbers)} called."); + var result = new ComplexNumber(0, 0); + foreach (ComplexNumber number in numbers) { - A = a; - B = b; + result = new ComplexNumber(result.A + number.A, result.B + number.B); } + return result; } - public enum TextStyle + public async Task AddFloat(float x, float y, CancellationToken cancellationToken = default) { - TitleCase, - Upper + //Trace.WriteLine($"{nameof(AddFloat)} called."); + _logger.LogInformation($"{nameof(AddFloat)} called."); + return x + y; } - 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 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 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(); + 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 index 389cb15d..41a9292f 100644 --- a/src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs +++ b/src/UiPath.CoreIpc.Tests/Implementation/IpcHelpers.cs @@ -1,56 +1,77 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Diagnostics; -using System.Linq; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.WebSockets; using UiPath.CoreIpc.Tests; -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; + +public static class IpcHelpers { - public static class IpcHelpers + public static TInterface ValidateAndBuild(this ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder { - public static TInterface ValidateAndBuild(this ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder - { #if DEBUG - Validator.Validate(builder); + Validator.Validate(builder); #endif - return builder.Build(); - } - public static ServiceHost ValidateAndBuild(this ServiceHostBuilder serviceHostBuilder) - { + return builder.Build(); + } + public static ServiceHost ValidateAndBuild(this ServiceHostBuilder serviceHostBuilder) + { #if DEBUG - Validator.Validate(serviceHostBuilder); + 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) + 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 => { - string userName = null; - client.Impersonate(() => userName = Environment.UserName); - return userName; - } - public static IServiceCollection AddIpcWithLogging(this IServiceCollection services, bool logToConsole = false) + //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) { - services.AddLogging(builder => + var listenerContext = await _httpListener.GetContextAsync(); + if (listenerContext.Request.IsWebSocketRequest) { - 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(); + 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 index ec230fda..b63c41c6 100644 --- a/src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs +++ b/src/UiPath.CoreIpc.Tests/Implementation/OneWayStreamWrapper.cs @@ -1,109 +1,104 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +internal class OneWayStreamWrapper : Stream { - internal class OneWayStreamWrapper : Stream - { - private readonly Stream innerStream; - private readonly bool canRead; - private readonly bool canWrite; + private readonly Stream innerStream; + private readonly bool canRead; + private readonly bool canWrite; - internal OneWayStreamWrapper(Stream innerStream, bool canRead = false, bool canWrite = false) + internal OneWayStreamWrapper(Stream innerStream, bool canRead = false, bool canWrite = false) + { + if (canRead == canWrite) { - 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; + 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 CanRead => this.canRead && this.innerStream.CanRead; - public override bool CanSeek => false; + public override bool CanSeek => false; - public override bool CanWrite => this.canWrite && this.innerStream.CanWrite; + public override bool CanWrite => this.canWrite && this.innerStream.CanWrite; - public override long Length => throw new NotSupportedException(); + public override long Length => throw new NotSupportedException(); - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - public override void Flush() + public override void Flush() + { + if (this.CanWrite) + { + this.innerStream.Flush(); + } + else { - if (this.CanWrite) - { - this.innerStream.Flush(); - } - else - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); } + } - public override int Read(byte[] buffer, int offset, int count) + public override int Read(byte[] buffer, int offset, int count) + { + if (this.CanRead) + { + return this.innerStream.Read(buffer, offset, count); + } + else { - if (this.CanRead) - { - return this.innerStream.Read(buffer, offset, count); - } - else - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); } + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (this.CanRead) { - if (this.CanRead) - { - return this.innerStream.ReadAsync(buffer, offset, count, cancellationToken); - } - else - { - throw new NotSupportedException(); - } + 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 long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) + public override void Write(byte[] buffer, int offset, int count) + { + if (this.CanWrite) + { + this.innerStream.Write(buffer, offset, count); + } + else { - if (this.CanWrite) - { - this.innerStream.Write(buffer, offset, count); - } - else - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); } + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (this.CanWrite) + { + return this.innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + else { - if (this.CanWrite) - { - return this.innerStream.WriteAsync(buffer, offset, count, cancellationToken); - } - else - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); } + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this.innerStream.Dispose(); - } + 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 index f724f98c..049f4e36 100644 --- a/src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs +++ b/src/UiPath.CoreIpc.Tests/Implementation/SystemService.cs @@ -1,179 +1,175 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; +using System.Globalization; using System.Text; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.Tests +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 interface ISystemService + public string Text { get; set; } + public int Delay { get; set; } +} +public class SystemService : ISystemService +{ + public SystemService() { - 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 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 async Task Infinite(CancellationToken cancellationToken = default) { - public string Text { get; set; } - public int Delay { get; set; } + await Task.Delay(Timeout.Infinite, cancellationToken); + return true; } - 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) + public async Task ConvertText(string text, TextStyle style, CancellationToken cancellationToken = default) + { + switch (style) { - switch (style) - { - case TextStyle.TitleCase: - return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(text); - case TextStyle.Upper: - return CultureInfo.InvariantCulture.TextInfo.ToUpper(text); - default: - return text; - } + 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 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 bool DidNothing { get; set; } - public async Task DoNothing(CancellationToken cancellationToken = default) - { - const int Timeout = + public async Task DoNothing(CancellationToken cancellationToken = default) + { + const int Timeout = #if CI - 100; + 100; #else - 10; + 10; #endif - await Task.Delay(Timeout); - DidNothing = true; - } + await Task.Delay(Timeout); + DidNothing = true; + } - public async Task GetGuid(Guid guid, CancellationToken cancellationToken = default) - { - //throw new Exception("sssss"); - return guid; - } + 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 ReverseBytes(byte[] input, CancellationToken cancellationToken = default) + { + return input.Reverse().ToArray(); + } - public async Task MissingCallback(SystemMessage message, CancellationToken cancellationToken = default) + public async Task MissingCallback(SystemMessage message, CancellationToken cancellationToken = default) + { + if (message.Delay != 0) { - 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; + 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) + public async Task SlowOperation(CancellationToken cancellationToken = default) + { + Console.WriteLine("SlowOperation " + Thread.CurrentThread.Name); + try { - Console.WriteLine("SlowOperation " + Thread.CurrentThread.Name); - try + for(int i = 0; i < 5; i++) { - for(int i = 0; i < 5; i++) + await Task.Delay(1000, cancellationToken); + Console.WriteLine("SlowOperation "+Thread.CurrentThread.Name); + if(cancellationToken.IsCancellationRequested) { - await Task.Delay(1000, cancellationToken); - Console.WriteLine("SlowOperation "+Thread.CurrentThread.Name); - if(cancellationToken.IsCancellationRequested) - { - Console.WriteLine("SlowOperation Cancelled."); - return false; - } + 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; } + catch(Exception ex) + { + Console.WriteLine(ex.ToString()); + } + Console.WriteLine("SlowOperation finished. "+ (cancellationToken.IsCancellationRequested ? "cancelled " : "") + Thread.CurrentThread.Name); + return true; + } - public string ThreadName; + public string ThreadName; - public Task VoidSyncThrow(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task VoidSyncThrow(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public async Task VoidThreadName(CancellationToken cancellationToken = default) => ThreadName = Thread.CurrentThread.Name; + public async Task VoidThreadName(CancellationToken cancellationToken = default) => ThreadName = Thread.CurrentThread.Name; - public async Task GetThreadName(CancellationToken cancellationToken = default) => 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 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 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 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 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; - } + 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 index 8b59da38..e81f1eaa 100644 --- a/src/UiPath.CoreIpc.Tests/NamedPipeTests.cs +++ b/src/UiPath.CoreIpc.Tests/NamedPipeTests.cs @@ -1,60 +1,53 @@ -using System; -using System.IO.Pipes; +using System.IO.Pipes; using System.Security.Principal; -using System.Threading.Tasks; -using Shouldly; -using UiPath.CoreIpc.NamedPipe; -using Xunit; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +public class SystemNamedPipeTests : SystemTests> { - 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() { - 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(), 30).ShouldBeTrue(); - } - [Fact] - public Task ServerName() => SystemClientBuilder().ServerName(Environment.MachineName).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 + IOHelpers.PipeExists(System.Guid.NewGuid().ToString()).ShouldBeFalse(); + IOHelpers.PipeExists("system"+GetHashCode(), 50).ShouldBeTrue(); } - public class ComputingNamedPipeTests : ComputingTests> + [Fact] + public Task ServerName() => SystemClientBuilder().ValidateAndBuild().GetGuid(System.Guid.Empty); + [Fact] + public override void BeforeCallServerSide() { - protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => - serviceHostBuilder.UseNamedPipes(Configure(new NamedPipeSettings("computing"+GetHashCode()){ EncryptAndSign = true })); - protected override NamedPipeClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => - new NamedPipeClientBuilder("computing" + GetHashCode(), _serviceProvider) - .AllowImpersonation() - .EncryptAndSign() - .RequestTimeout(RequestTimeout) - .CallbackInstance(_computingCallback) - .TaskScheduler(taskScheduler); + _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 index 97a73415..b30d6f93 100644 --- a/src/UiPath.CoreIpc.Tests/NestedStreamTests.cs +++ b/src/UiPath.CoreIpc.Tests/NestedStreamTests.cs @@ -1,378 +1,370 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace UiPath.CoreIpc.Tests +using System.IO.Compression; + +namespace UiPath.CoreIpc.Tests; + +public class NestedStreamTests { - public class NestedStreamTests - { - private const int DefaultNestedLength = 10; + private const int DefaultNestedLength = 10; - private MemoryStream underlyingStream; + private MemoryStream underlyingStream; - private NestedStream stream; + private NestedStream stream; - protected static readonly TimeSpan UnexpectedTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); + protected static readonly TimeSpan UnexpectedTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); - private readonly CancellationTokenSource _timeoutTokenSource = new(UnexpectedTimeout); + 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); - } + 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; + 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() + { + 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); + [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); - } + Assert.False(stream.CanSeek); + stream.Dispose(); + Assert.False(stream.CanSeek); + } - [Fact] - public void Length() - { - Assert.Equal(DefaultNestedLength, this.stream.Length); - } + [Fact] + public void Length() + { + Assert.Equal(DefaultNestedLength, this.stream.Length); + } - [Fact] - public void Length_NonSeekableStream() + [Fact] + public void Length_NonSeekableStream() + { + using (var gzipStream = new GZipStream(Stream.Null, CompressionMode.Decompress)) + using (var stream = gzipStream.ReadSlice(10)) { - using (var gzipStream = new GZipStream(Stream.Null, CompressionMode.Decompress)) - using (var stream = gzipStream.ReadSlice(10)) - { - Assert.Throws(() => stream.Length); - } + 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); + [Fact] + public void Position() + { + byte[] buffer = new byte[DefaultNestedLength]; - 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); - } + Assert.Equal(0, this.stream.Position); + var bytesRead = this.stream.Read(buffer, 0, 5); + Assert.Equal(bytesRead, this.stream.Position); - [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); - } + 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 Dispose_IncompleteDisposesUnderylingStream() - { - this.stream.Dispose(); - Assert.False(this.underlyingStream.CanSeek); - } + [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 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 IsDisposed() + { + Assert.False(stream.IsDisposed); + this.stream.Dispose(); + Assert.True(stream.IsDisposed); + } - [Fact] - public void SetLength() - { - Assert.Throws(() => this.stream.SetLength(0)); - } + [Fact] + public void Dispose_IncompleteDisposesUnderylingStream() + { + this.stream.Dispose(); + Assert.False(this.underlyingStream.CanSeek); + } - [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 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 Sook_WithNonStartPositionInUnderlyingStream() - { - this.underlyingStream.Position = 1; - this.stream = this.underlyingStream.ReadSlice(5); + [Fact] + public void SetLength() + { + Assert.Throws(() => this.stream.SetLength(0)); + } - 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_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 Seek_Begin() - { - Assert.Equal(0, this.stream.Position); - Assert.Throws(() => this.stream.Seek(-1, SeekOrigin.Begin)); - Assert.Equal(0, this.underlyingStream.Position); + [Fact] + public void Sook_WithNonStartPositionInUnderlyingStream() + { + this.underlyingStream.Position = 1; + this.stream = this.underlyingStream.ReadSlice(5); - Assert.Equal(0, this.stream.Seek(0, SeekOrigin.Begin)); - Assert.Equal(0, this.underlyingStream.Position); + Assert.Equal(0, this.stream.Position); + Assert.Equal(2, this.stream.Seek(2, SeekOrigin.Current)); + Assert.Equal(3, this.underlyingStream.Position); + } - Assert.Equal(5, this.stream.Seek(5, SeekOrigin.Begin)); - Assert.Equal(5, 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(DefaultNestedLength, this.stream.Seek(DefaultNestedLength, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); + Assert.Equal(0, this.stream.Seek(0, SeekOrigin.Begin)); + Assert.Equal(0, this.underlyingStream.Position); - Assert.Equal(DefaultNestedLength + 1, this.stream.Seek(DefaultNestedLength + 1, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength + 1, this.underlyingStream.Position); + Assert.Equal(5, this.stream.Seek(5, SeekOrigin.Begin)); + Assert.Equal(5, this.underlyingStream.Position); - this.stream.Dispose(); - Assert.Throws(() => this.stream.Seek(0, SeekOrigin.Begin)); - } + Assert.Equal(DefaultNestedLength, this.stream.Seek(DefaultNestedLength, SeekOrigin.Begin)); + Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - [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 + 1, this.stream.Seek(DefaultNestedLength + 1, SeekOrigin.Begin)); + Assert.Equal(DefaultNestedLength + 1, this.underlyingStream.Position); - Assert.Equal(DefaultNestedLength, this.stream.Seek(0, SeekOrigin.End)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); + this.stream.Dispose(); + Assert.Throws(() => this.stream.Seek(0, SeekOrigin.Begin)); + } - Assert.Equal(DefaultNestedLength + 5, this.stream.Seek(5, SeekOrigin.End)); - Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); + [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.Throws(() => this.stream.Seek(-20, SeekOrigin.Begin)); - Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); + Assert.Equal(DefaultNestedLength, this.stream.Seek(0, SeekOrigin.End)); + Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - this.stream.Dispose(); - Assert.Throws(() => this.stream.Seek(0, SeekOrigin.End)); - } + Assert.Equal(DefaultNestedLength + 5, this.stream.Seek(5, SeekOrigin.End)); + Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); - [Fact] - public void Flush() - { - Assert.Throws(() => this.stream.Flush()); - } + Assert.Throws(() => this.stream.Seek(-20, SeekOrigin.Begin)); + Assert.Equal(DefaultNestedLength + 5, this.underlyingStream.Position); - [Fact] - public async Task FlushAsync() - { - await Assert.ThrowsAsync(() => this.stream.FlushAsync()); - } + this.stream.Dispose(); + Assert.Throws(() => this.stream.Seek(0, SeekOrigin.End)); + } - [Fact] - public void CanRead() - { - Assert.True(this.stream.CanRead); - this.stream.Dispose(); - Assert.False(this.stream.CanRead); - } + [Fact] + public void Flush() + { + Assert.Throws(() => this.stream.Flush()); + } - [Fact] - public void CanWrite() - { - Assert.False(this.stream.CanWrite); - this.stream.Dispose(); - Assert.False(this.stream.CanWrite); - } + [Fact] + public async Task FlushAsync() + { + await Assert.ThrowsAsync(() => this.stream.FlushAsync()); + } - [Fact] - public async Task WriteAsync_Throws() - { - await Assert.ThrowsAsync(() => this.stream.WriteAsync(new byte[1], 0, 1)); - } + [Fact] + public void CanRead() + { + Assert.True(this.stream.CanRead); + this.stream.Dispose(); + Assert.False(this.stream.CanRead); + } - [Fact] - public void Write_Throws() - { - Assert.Throws(() => this.stream.Write(new byte[1], 0, 1)); - } + [Fact] + public void CanWrite() + { + Assert.False(this.stream.CanWrite); + this.stream.Dispose(); + Assert.False(this.stream.CanWrite); + } - [Fact] - public async Task ReadAsync_Empty_ReturnsZero() - { - Assert.Equal(0, await this.stream.ReadAsync(Array.Empty(), 0, 0, default)); - } + [Fact] + public async Task WriteAsync_Throws() + { + await Assert.ThrowsAsync(() => this.stream.WriteAsync(new byte[1], 0, 1)); + } - [Fact] - public async Task Read_BeyondEndOfStream_ReturnsZero() - { - // Seek beyond the end of the stream - this.stream.Seek(1, SeekOrigin.End); + [Fact] + public void Write_Throws() + { + Assert.Throws(() => this.stream.Write(new byte[1], 0, 1)); + } - byte[] buffer = new byte[this.underlyingStream.Length]; + [Fact] + public async Task ReadAsync_Empty_ReturnsZero() + { + Assert.Equal(0, await this.stream.ReadAsync(Array.Empty(), 0, 0, default)); + } - Assert.Equal(0, await this.stream.ReadAsync(buffer, 0, buffer.Length, this.TimeoutToken)); - } + [Fact] + public async Task Read_BeyondEndOfStream_ReturnsZero() + { + // Seek beyond the end of the stream + this.stream.Seek(1, SeekOrigin.End); - [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); + byte[] buffer = new byte[this.underlyingStream.Length]; - Assert.Equal(0, await this.stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead, this.TimeoutToken)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - } + Assert.Equal(0, await this.stream.ReadAsync(buffer, 0, buffer.Length, this.TimeoutToken)); + } - [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); + [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, this.stream.Read(buffer, bytesRead, buffer.Length - bytesRead)); - Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); - } + Assert.Equal(0, await this.stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead, this.TimeoutToken)); + Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); + } - [Fact] - public void Read_Empty_ReturnsZero() - { - Assert.Equal(0, this.stream.Read(Array.Empty(), 0, 0)); - } + [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); - [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)); - } + Assert.Equal(0, this.stream.Read(buffer, bytesRead, buffer.Length - bytesRead)); + Assert.Equal(DefaultNestedLength, this.underlyingStream.Position); + } - [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 Read_Empty_ReturnsZero() + { + Assert.Equal(0, this.stream.Read(Array.Empty(), 0, 0)); + } - [Fact] - public void CreationDoesNotReadFromUnderlyingStream() - { - Assert.Equal(0, this.underlyingStream.Position); - } + [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_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 void Read_WhenLengthIsInitially0() + { + this.stream = this.underlyingStream.ReadSlice(0); + Assert.Equal(0, this.stream.Read(new byte[1], 0, 1)); + } - [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 CreationDoesNotReadFromUnderlyingStream() + { + Assert.Equal(0, this.underlyingStream.Position); + } - [Fact] - public void Read_ValidatesArguments() - { - var buffer = new byte[20]; + [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)); + } - 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_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 async Task ReadAsync_ValidatesArguments() - { - var buffer = new byte[20]; + [Fact] + public void Read_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)); - } + 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)); } - public static class StreamExtensions + + [Fact] + public async Task ReadAsync_ValidatesArguments() { - /// - /// 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); + 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 index a288ed9c..e33832f4 100644 --- a/src/UiPath.CoreIpc.Tests/SystemTests.cs +++ b/src/UiPath.CoreIpc.Tests/SystemTests.cs @@ -1,275 +1,295 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Shouldly; -using Xunit; +using System.Text; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +public abstract class SystemTests : TestBase where TBuilder : ServiceClientBuilder { - public abstract class SystemTests : TestBase where TBuilder : ServiceClientBuilder + protected ServiceHost _systemHost; + protected ISystemService _systemClient; + protected readonly SystemService _systemService; + public SystemTests() { - 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); - } + _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() + [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) { - _systemService.DidNothing = false; - await _systemClient.DoNothing(); - _systemService.DidNothing.ShouldBeFalse(); - while (!_systemService.DidNothing) - { - await Task.Delay(10); - Trace.WriteLine(this + " Void"); - } + await Task.Delay(10); + Trace.WriteLine(this + " Void"); } + } - [Fact] - public async Task VoidThreadName() + [Fact] + public async Task VoidThreadName() + { + await _systemClient.VoidThreadName(); + await _systemClient.GetThreadName(); + while (_systemService.ThreadName != "GuiThread") { - await _systemClient.VoidThreadName(); - await _systemClient.GetThreadName(); - _systemService.ThreadName.ShouldBe("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 Enum() + { + var text = await _systemClient.ConvertText("hEllO woRd!", TextStyle.Upper); + text.ShouldBe("HELLO WORD!"); + } - [Fact] - public async Task MaxMessageSize() - { - _systemClient.ReverseBytes(new byte[MaxReceivedMessageSizeInMegabytes * 1024 * 1024]).ShouldThrow(); - await Guid(); - } + [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 Guid() - { - var newGuid = System.Guid.NewGuid(); - var guid = await _systemClient.GetGuid(newGuid); - guid.ShouldBe(newGuid); - } + [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 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 ReverseBytes() + [Fact] + public async Task MissingCallback() + { + RemoteException exception = null; + try { - var input = Encoding.UTF8.GetBytes("Test"); - var reversed = await _systemClient.ReverseBytes(input); - reversed.ShouldBe(input.Reverse()); + await _systemClient.MissingCallback(new SystemMessage()); } - - [Fact] - public async Task MissingCallback() + catch (RemoteException ex) { - 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(); + 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 VoidIsAsync() => await _systemClient.VoidSyncThrow(); - [Fact] - public async Task GetThreadName() => (await _systemClient.GetThreadName()).ShouldBe("GuiThread"); + [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 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() + [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)) { - 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(); - } + _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"); + [Fact] + public async Task Upload() + { + (await _systemClient.Upload(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBe("Hello world"); + await Guid(); + } - [Fact] - public async Task UploadNoRead() + [Fact] + public virtual async Task UploadNoRead() + { + try { - try - { - (await _systemClient.UploadNoRead(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBeEmpty(); - } - catch (IOException) { } - await Guid(); + (await _systemClient.UploadNoRead(new MemoryStream(Encoding.UTF8.GetBytes("Hello world")))).ShouldBeEmpty(); } + catch (IOException) { } + catch (ObjectDisposedException) { } + await Guid(); + } - [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().RequestTimeout(RequestTimeout).Logger(_serviceProvider); - [Fact] - public async Task BeforeCall() + [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, _) => { - bool newConnection = false; - var proxy = SystemClientBuilder().BeforeCall(async (c, _) => - { - newConnection = c.NewConnection; - c.MethodName.ShouldBe(nameof(ISystemService.DoNothing)); - c.Arguments.Single().ShouldBe(""); // cancellation token - }).ValidateAndBuild(); - newConnection.ShouldBeFalse(); + 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.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(); + var ipcProxy = (IpcProxy)proxy; + var closed = false; + ipcProxy.Connection.Closed += delegate { closed = true; }; + ipcProxy.CloseConnection(); + closed.ShouldBeTrue(); + newConnection.ShouldBeFalse(); + await proxy.DoNothing(); + newConnection.ShouldBeTrue(); - await proxy.DoNothing(); - newConnection.ShouldBeFalse(); - ipcProxy.CloseConnection(); - } + await proxy.DoNothing(); + newConnection.ShouldBeFalse(); + ipcProxy.CloseConnection(); + } - [Fact] - public async Task DontReconnect() + [Fact] + public async Task DontReconnect() + { + var proxy = SystemClientBuilder().DontReconnect().ValidateAndBuild(); + await proxy.GetGuid(System.Guid.Empty); + ((IpcProxy)proxy).CloseConnection(); + ObjectDisposedException exception = null; + try { - 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) + catch (ObjectDisposedException ex) { - 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); - } + exception = ex; } - [Fact] - public virtual async void BeforeCallServerSide() + 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(); - var methodName = ""; - using var protectedService = Configure(new ServiceHostBuilder(_serviceProvider)) - .AddEndpoint(new EndpointSettings - { - BeforeCall = async (call, ct) => - { - methodName = call.MethodName; - call.Arguments[0].ShouldBe(newGuid); - } - }) - .ValidateAndBuild(); - _ = protectedService.RunAsync(); - await CreateSystemService().GetGuid(newGuid); - methodName.ShouldBe("GetGuid"); + (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 index 15ed0462..cca919c5 100644 --- a/src/UiPath.CoreIpc.Tests/TcpTests..cs +++ b/src/UiPath.CoreIpc.Tests/TcpTests..cs @@ -1,33 +1,28 @@ using System.Net; -using System.Threading.Tasks; using UiPath.CoreIpc.Tcp; -using Xunit; - -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; +public class SystemTcpTests : SystemTests> { - 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() { - 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))); + _port++; + base.BeforeCallServerSide(); } + IPEndPoint GetEndPoint() => new(IPAddress.Loopback, _port); +} +public class ComputingTcpTests : ComputingTests> +{ + protected static readonly IPEndPoint ComputingEndPoint = new(IPAddress.Loopback, 2121+GetCount()); + protected override TcpClientBuilder ComputingClientBuilder(TaskScheduler taskScheduler = null) => + new TcpClientBuilder(ComputingEndPoint, _serviceProvider) + .RequestTimeout(RequestTimeout) + .CallbackInstance(_computingCallback) + .TaskScheduler(taskScheduler); + protected override ServiceHostBuilder Configure(ServiceHostBuilder serviceHostBuilder) => + serviceHostBuilder.UseTcp(Configure(new TcpSettings(ComputingEndPoint))); } \ No newline at end of file diff --git a/src/UiPath.CoreIpc.Tests/TestBase.cs b/src/UiPath.CoreIpc.Tests/TestBase.cs index 873db57b..98d75a4e 100644 --- a/src/UiPath.CoreIpc.Tests/TestBase.cs +++ b/src/UiPath.CoreIpc.Tests/TestBase.cs @@ -1,39 +1,39 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Nito.AsyncEx; +using Nito.AsyncEx; -namespace UiPath.CoreIpc.Tests +namespace UiPath.CoreIpc.Tests; + +public abstract class TestBase : IDisposable { - public abstract class TestBase : IDisposable - { - protected const int MaxReceivedMessageSizeInMegabytes = 1; - protected static int Count = -1; - public static readonly TimeSpan RequestTimeout = + protected const int MaxReceivedMessageSizeInMegabytes = 1; + protected static int Count = -1; + public static readonly TimeSpan RequestTimeout = #if CI - TimeSpan.FromSeconds(2) + + TimeSpan.FromSeconds(2) + #endif - TimeSpan.FromSeconds(2); - protected readonly IServiceProvider _serviceProvider; - protected readonly AsyncContext _guiThread = new AsyncContextThread().Context; + (Debugger.IsAttached ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(2)); + protected readonly IServiceProvider _serviceProvider; + protected readonly AsyncContext _guiThread = new AsyncContextThread().Context; - public TestBase() - { - _guiThread.SynchronizationContext.Send(() => Thread.CurrentThread.Name = "GuiThread"); - _serviceProvider = IpcHelpers.ConfigureServices(); - } + //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 static int GetCount() => Interlocked.Increment(ref Count); - protected TaskScheduler GuiScheduler => _guiThread.Scheduler; + 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); + 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 index 28b7fa05..eaf03da6 100644 --- a/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj +++ b/src/UiPath.CoreIpc.Tests/UiPath.CoreIpc.Tests.csproj @@ -1,19 +1,30 @@  - net461;net5.0;net5.0-windows + 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 index ceb5dcff..a1a0197c 100644 --- a/src/UiPath.CoreIpc.Tests/ValidationTests.cs +++ b/src/UiPath.CoreIpc.Tests/ValidationTests.cs @@ -1,51 +1,48 @@ -using Shouldly; -using System; -using System.IO; -using UiPath.CoreIpc.NamedPipe; -using Xunit; +namespace UiPath.CoreIpc.Tests; -namespace UiPath.CoreIpc.Tests +public class ValidationTests { - public class ValidationTests + class JobFailedException : Exception { - class JobFailedException : Exception + public JobFailedException(Error error) : base("Job has failed.", new RemoteException(error)) { - public JobFailedException(Error error) : base("Job has failed.", new RemoteException(error)) - { - } } + } - [Fact] - public void ErrorFromRemoteException() - { - var innerError = new Error(new InvalidDataException("invalid")); - var error = new Error(new JobFailedException(innerError)); - 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 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!"); + [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 new file mode 100644 index 00000000..0661160c --- /dev/null +++ b/src/UiPath.CoreIpc.Tests/WebSocketTests.cs @@ -0,0 +1,49 @@ +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/CancellationTokenSourcePool.cs b/src/UiPath.CoreIpc/CancellationTokenSourcePool.cs new file mode 100644 index 00000000..a6d0b3f8 --- /dev/null +++ b/src/UiPath.CoreIpc/CancellationTokenSourcePool.cs @@ -0,0 +1,50 @@ +namespace UiPath.CoreIpc; +// https://github.com/dotnet/aspnetcore/blob/main/src/Shared/CancellationTokenSourcePool.cs +internal static class CancellationTokenSourcePool +{ + public static PooledCancellationTokenSource Rent() => +#if !NET461 + ObjectPool.Rent(); +#else + new(); +#endif + static bool Return(PooledCancellationTokenSource cts) => ObjectPool.Return(cts); + public sealed class PooledCancellationTokenSource : CancellationTokenSource + { + public void Return() + { + // If we failed to return to the pool then dispose + #if !NET461 + if (!TryReset() || !CancellationTokenSourcePool.Return(this)) + #endif + { + Dispose(); + } + } + } +} +static class ObjectPool where T : new() +{ + private const int MaxQueueSize = 1024; + private static readonly ConcurrentQueue Cache = new(); + private static int Count; + public static T Rent() + { + if (Cache.TryDequeue(out var cts)) + { + Interlocked.Decrement(ref Count); + return cts; + } + return new(); + } + public static bool Return(T item) + { + if (Interlocked.Increment(ref Count) > MaxQueueSize) + { + Interlocked.Decrement(ref Count); + return false; + } + Cache.Enqueue(item); + return true; + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs b/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs index 6a8ed622..14b708fc 100644 --- a/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs +++ b/src/UiPath.CoreIpc/Client/ClientConnectionsRegistry.cs @@ -1,124 +1,98 @@ -using Microsoft.Extensions.Logging; -using Nito.AsyncEx; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace UiPath.CoreIpc; -namespace UiPath.CoreIpc +static class ClientConnectionsRegistry { - static class ClientConnectionsRegistry + private static readonly ConcurrentDictionary Connections = new(); + public static async Task GetOrCreate(IConnectionKey key, CancellationToken cancellationToken) { - private static readonly ConcurrentDictionaryWrapper _connections = new(CreateClientConnection); - public static async Task GetOrCreate(IConnectionKey key, CancellationToken cancellationToken) + var clientConnection = GetOrAdd(key); + await clientConnection.Lock(cancellationToken); + try { - var clientConnection = GetOrAdd(key); - var asyncLock = 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) { - // check again just in case it was removed after GetOrAdd but before entering the lock - ClientConnection newClientConnection; - while ((newClientConnection = GetOrAdd(key)) != clientConnection) - { - asyncLock.Dispose(); - asyncLock = await newClientConnection.Lock(cancellationToken); - clientConnection = newClientConnection; - } + clientConnection.Release(); + await newClientConnection.Lock(cancellationToken); + clientConnection = newClientConnection; } - catch - { - asyncLock.Dispose(); - throw; - } - return new(clientConnection, asyncLock); } - private static ClientConnection GetOrAdd(IConnectionKey key)=>_connections.GetOrAdd(key); - static ClientConnection CreateClientConnection(IConnectionKey key) => key.CreateClientConnection(key); - public static bool TryGet(IConnectionKey key, out ClientConnection connection) => _connections.TryGetValue(key, out connection); - internal static ClientConnection Remove(IConnectionKey connectionKey) + catch { - _connections.TryRemove(connectionKey, out var clientConnection); - return clientConnection; + clientConnection.Release(); + throw; } + return clientConnection; } - readonly struct ClientConnectionHandle : IDisposable + 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) { - private readonly IDisposable _asyncLock; - public ClientConnectionHandle(ClientConnection clientConnection, IDisposable asyncLock) - { - ClientConnection = clientConnection; - _asyncLock = asyncLock; - } - public ClientConnection ClientConnection { get; } - public void Dispose() => _asyncLock.Dispose(); + Connections.TryRemove(connectionKey, out var clientConnection); + return clientConnection; } - interface IConnectionKey : IEquatable +} +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 { - bool EncryptAndSign { get; } - ClientConnection CreateClientConnection(IConnectionKey key); + get => _connection; + set + { + _connection = value; + _connection.Closed += OnConnectionClosed; + } } - abstract class ClientConnection : IDisposable + public abstract Task Connect(CancellationToken cancellationToken); + private void OnConnectionClosed(object sender, EventArgs _) { - readonly AsyncLock _lock = new(); - Connection _connection; - protected ClientConnection(IConnectionKey connectionKey) => ConnectionKey = connectionKey; - public abstract bool Connected { get; } - public abstract Stream Network { get; } - public Connection Connection + var closedConnection = (Connection)sender; + if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out var clientConnection) || clientConnection.Connection != closedConnection) { - get => _connection; - set - { - _connection = value; - _connection.Closed += OnConnectionClosed; - } + return; } - public abstract Task Connect(CancellationToken cancellationToken); - private void OnConnectionClosed(object sender, EventArgs _) + if (!clientConnection.TryLock()) { - var closedConnection = (Connection)sender; - if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out var clientConnection) || clientConnection.Connection != closedConnection) - { - return; - } - if (!clientConnection.TryLock(out var guard)) - { - return; - } - using (guard) - { - if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out clientConnection) || clientConnection.Connection != closedConnection) - { - return; - } - var removedConnection = ClientConnectionsRegistry.Remove(ConnectionKey); - _connection.Logger?.LogInformation($"Remove connection {removedConnection}."); - Debug.Assert(removedConnection?.Connection == closedConnection, "Removed the wrong connection."); - } + return; } - public Server Server { get; set; } - protected IConnectionKey ConnectionKey { get; } - public Task Lock(CancellationToken cancellationToken = default) => _lock.LockAsync(cancellationToken); - public bool TryLock(out IDisposable guard) + try { - try + if (!ClientConnectionsRegistry.TryGet(ConnectionKey, out clientConnection) || clientConnection.Connection != closedConnection) { - guard = _lock.Lock(new(canceled: true)); - return true; + return; } - catch (TaskCanceledException) + var removedConnection = ClientConnectionsRegistry.Remove(ConnectionKey); + if (_connection.LogEnabled) { - guard = null; - return false; + _connection.Log($"Remove connection {removedConnection}."); } + Debug.Assert(removedConnection?.Connection == closedConnection, "Removed the wrong connection."); } - public override string ToString() => _connection?.ToString() ?? base.ToString(); - protected virtual void Dispose(bool disposing) {} - public void Dispose() + finally { - Dispose(disposing: true); - GC.SuppressFinalize(this); + 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/ServiceClient.cs b/src/UiPath.CoreIpc/Client/ServiceClient.cs index 41a68d46..8ee847f5 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClient.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClient.cs @@ -1,291 +1,317 @@ -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using System.Runtime.ExceptionServices; -using System.Net.Security; -using System.Security.Principal; -using System.Diagnostics; +using System.Net.Security; +namespace UiPath.CoreIpc; -namespace UiPath.CoreIpc +using ConnectionFactory = Func>; +using BeforeCallHandler = Func; +using InvokeDelegate = Func; + +interface IServiceClient : IDisposable +{ + Task Invoke(MethodInfo method, object[] args); + Connection Connection { get; } +} + +class ServiceClient : IServiceClient, IConnectionKey where TInterface : class { - using ConnectionFactory = Func>; - using BeforeCallHandler = Func; - using InvokeDelegate = Func; + 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; - interface IServiceClient : IDisposable + internal ServiceClient(ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, string sslServer = null, BeforeCallHandler beforeCall = null, bool objectParameters = false, EndpointSettings serviceEndpoint = null) { - Task Invoke(string methodName, object[] args); - Connection Connection { get; } + ObjectParameters = objectParameters; + _serializer = serializer; + _requestTimeout = requestTimeout; + _logger = logger; + _connectionFactory = connectionFactory; + SslServer = sslServer; + _beforeCall = beforeCall; + _serviceEndpoint = serviceEndpoint; } + 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; } - class ServiceClient : IServiceClient, IConnectionKey where TInterface : class + public TInterface CreateProxy() { - 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 AsyncLock _connectionLock = new(); - private Connection _connection; - private Server _server; - private ClientConnection _clientConnection; - - internal ServiceClient(ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, bool encryptAndSign = false, BeforeCallHandler beforeCall = null, EndpointSettings serviceEndpoint = null) - { - _serializer = serializer; - _requestTimeout = requestTimeout; - _logger = logger; - _connectionFactory = connectionFactory; - EncryptAndSign = encryptAndSign; - _beforeCall = beforeCall; - _serviceEndpoint = serviceEndpoint; - } - - public virtual string Name => _connection?.Name; - - public bool EncryptAndSign { get; } - - Connection IServiceClient.Connection => _connection; + var proxy = DispatchProxy.Create(); + (proxy as IpcProxy).ServiceClient = this; + return proxy; + } + + public override int GetHashCode() => HashCode; - public TInterface CreateProxy() + private void OnNewConnection(Connection connection, bool alreadyHasServer = false) + { + _connection?.Dispose(); + _connection = connection; + if (alreadyHasServer || _serviceEndpoint == null) { - var proxy = DispatchProxy.Create(); - (proxy as IpcProxy).ServiceClient = this; - return proxy; + return; } + 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); + } - private async Task CreateConnection(Stream network) + public Task Invoke(MethodInfo method, object[] args) + { + var syncContext = SynchronizationContext.Current; + var defaultContext = syncContext == null || syncContext.GetType() == typeof(SynchronizationContext); + return defaultContext ? Invoke() : Task.Run(Invoke); + async Task Invoke() { - var stream = EncryptAndSign ? await AuthenticateAsClient() : network; - OnNewConnection(new(stream, _serializer, _logger, Name)); - _logger?.LogInformation($"CreateConnection {Name}."); - return; - async Task AuthenticateAsClient() + CancellationToken cancellationToken = default; + TimeSpan messageTimeout = default; + TimeSpan clientTimeout = _requestTimeout; + Stream uploadStream = null; + string[] serializedArguments = null; + var methodName = method.Name; + SerializeArguments(); + var timeoutHelper = new TimeoutHelper(clientTimeout, cancellationToken); + try { - var negotiateStream = new NegotiateStream(network); + var token = timeoutHelper.Token; + bool newConnection; + await _connectionLock.WaitAsync(token); try { - await negotiateStream.AuthenticateAsClientAsync(new(), "", ProtectionLevel.EncryptAndSign, TokenImpersonationLevel.Identification); + newConnection = await EnsureConnection(token); + } + finally + { + _connectionLock.Release(); + } + if (_beforeCall != null) + { + await _beforeCall(new(newConnection, method, args), token); + } + var requestId = _connection.NewRequestId(); + var request = new Request(typeof(TInterface).Name, requestId, methodName, serializedArguments, ObjectParameters ? args : null, messageTimeout.TotalSeconds) + { + UploadStream = uploadStream + }; + if (LogEnabled) + { + Log($"IpcClient calling {methodName} {requestId} {Name}."); } - catch + if (ObjectParameters && !method.ReturnType.IsGenericType) { - negotiateStream.Dispose(); - throw; + await _connection.Send(request, token); + return default; } - Debug.Assert(negotiateStream.IsEncrypted && negotiateStream.IsSigned); - return negotiateStream; + var response = await _connection.RemoteCall(request, token); + if (LogEnabled) + { + Log($"IpcClient called {methodName} {requestId} {Name}."); + } + return response.Deserialize(_serializer, ObjectParameters); } - } - - private void OnNewConnection(Connection connection, bool alreadyHasServer = false) - { - _connection?.Dispose(); - _connection = connection; - if (alreadyHasServer || _serviceEndpoint == null) + catch (Exception ex) + { + timeoutHelper.ThrowTimeout(ex, methodName); + throw; + } + finally { - return; + timeoutHelper.Dispose(); } - 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); - } - - public Task Invoke(string methodName, object[] args) - { - return Task.Run(Invoke); - Task Invoke() + void SerializeArguments() { - CancellationToken cancellationToken = default; - TimeSpan messageTimeout = default; - TimeSpan clientTimeout = _requestTimeout; - Stream uploadStream = null; - SetWellKnownArguments(); - return clientTimeout.Timeout(new() { cancellationToken }, async token => + if (!ObjectParameters) { - bool newConnection; - using (await _connectionLock.LockAsync(token)) - { - newConnection = await EnsureConnection(token); - } - if (_beforeCall != null) - { - await _beforeCall(new(newConnection, methodName, args), token); - } - var requestId = _connection.NewRequestId(); - var arguments = args.Select(_serializer.Serialize).ToArray(); - var request = new Request(typeof(TInterface).Name, requestId, methodName, arguments, messageTimeout.TotalSeconds); - _logger?.LogInformation($"IpcClient calling {methodName} {requestId} {Name}."); - var response = await _connection.RemoteCall(request, uploadStream, token); - _logger?.LogInformation($"IpcClient called {methodName} {requestId} {Name}."); - if (response.DownloadStream != null) - { - return (TResult)(object)response.DownloadStream; - } - return _serializer.Deserialize(response.CheckError().Data ?? ""); - }, methodName, ex => + serializedArguments = new string[args.Length]; + } + for (int index = 0; index < args.Length; index++) { - var exception = ex; - if (cancellationToken.IsCancellationRequested && !(ex is TaskCanceledException)) + switch (args[index]) { - exception = new TaskCanceledException(methodName, ex); + case Message { RequestTimeout: var requestTimeout } when requestTimeout != TimeSpan.Zero: + messageTimeout = requestTimeout; + clientTimeout = requestTimeout; + break; + case CancellationToken token: + cancellationToken = token; + args[index] = ""; + break; + case Stream stream: + uploadStream = stream; + args[index] = ""; + break; } - ExceptionDispatchInfo.Capture(exception).Throw(); - return Task.CompletedTask; - }); - void SetWellKnownArguments() - { - for (int index = 0; index < args.Length; index++) + if (!ObjectParameters) { - switch (args[index]) - { - case Message { RequestTimeout: var requestTimeout } when requestTimeout != TimeSpan.Zero: - messageTimeout = requestTimeout; - clientTimeout = requestTimeout; - break; - case CancellationToken token: - cancellationToken = token; - args[index] = ""; - break; - case Stream stream: - uploadStream = stream; - args[index] = ""; - break; - } + serializedArguments[index] = _serializer.Serialize(args[index]); } } } } + } - private async Task EnsureConnection(CancellationToken cancellationToken) + private async Task EnsureConnection(CancellationToken cancellationToken) + { + if (_connectionFactory != null) { - if (_connectionFactory != null) + var externalConnection = await _connectionFactory(_connection, cancellationToken); + if (externalConnection != null) { - var externalConnection = await _connectionFactory(_connection, cancellationToken); - if (externalConnection != null) + if (_connection == null) { - if (_connection == null) - { - OnNewConnection(externalConnection); - return true; - } - return false; + OnNewConnection(externalConnection); + return true; } + return false; } - return await CheckConnection(cancellationToken); } + if (_clientConnection?.Connected is true) + { + return false; + } + return await Connect(cancellationToken); + } - private async Task CheckConnection(CancellationToken cancellationToken) + private async Task Connect(CancellationToken cancellationToken) + { + var clientConnection = await ClientConnectionsRegistry.GetOrCreate(this, cancellationToken); + try { - if (_clientConnection?.Connected is true) - { - return false; - } - using var connectionHandle = await ClientConnectionsRegistry.GetOrCreate(this, cancellationToken); - var clientConnection = connectionHandle.ClientConnection; if (clientConnection.Connected) { ReuseClientConnection(clientConnection); return false; } clientConnection.Dispose(); + Stream network; try { - await clientConnection.Connect(cancellationToken); + network = await clientConnection.Connect(cancellationToken); } catch { clientConnection.Dispose(); throw; } - await InitializeClientConnection(clientConnection); - return true; + var stream = SslServer == null ? network : await AuthenticateAsClient(network); + OnNewConnection(new(stream, _serializer, _logger, Name)); + if (LogEnabled) + { + Log($"CreateConnection {Name}."); + } + InitializeClientConnection(clientConnection); } - - private void ReuseClientConnection(ClientConnection clientConnection) + finally { - _clientConnection = clientConnection; - var alreadyHasServer = clientConnection.Server != null; - _logger?.LogInformation(nameof(ReuseClientConnection)+" "+clientConnection); - OnNewConnection(clientConnection.Connection, alreadyHasServer); - if (!alreadyHasServer) + clientConnection.Release(); + } + return true; + async Task AuthenticateAsClient(Stream network) + { + var sslStream = new SslStream(network); + try { - clientConnection.Server = _server; + await sslStream.AuthenticateAsClientAsync(SslServer); } - else if (_serviceEndpoint != null) + catch { - _server = clientConnection.Server; - try - { - _server.Endpoints.Add(_serviceEndpoint.Name, _serviceEndpoint); - } - catch (ArgumentException ex) - { - throw new InvalidOperationException($"Duplicate callback proxy instance {Name} <{typeof(TInterface).Name}, {_serviceEndpoint.Contract.Name}>. Consider using a singleton callback proxy.", ex); - } + sslStream.Dispose(); + throw; } + Debug.Assert(sslStream.IsEncrypted && sslStream.IsSigned); + return sslStream; } + } - private async Task InitializeClientConnection(ClientConnection clientConnection) + private void ReuseClientConnection(ClientConnection clientConnection) + { + _clientConnection = clientConnection; + var alreadyHasServer = clientConnection.Server != null; + if (LogEnabled) { - await CreateConnection(clientConnection.Network); - _connection.Listen().LogException(_logger, Name); - clientConnection.Connection = _connection; - clientConnection.Server = _server; - _clientConnection = clientConnection; + Log(nameof(ReuseClientConnection) + " " + clientConnection); } - - public void Dispose() + OnNewConnection(clientConnection.Connection, alreadyHasServer); + if (!alreadyHasServer) { - Dispose(true); - GC.SuppressFinalize(this); + clientConnection.Server = _server; } - - protected virtual void Dispose(bool disposing) + else if (_serviceEndpoint != null) { - _logger?.LogInformation($"Dispose {Name}"); - if (disposing) + _server = clientConnection.Server; + if (_server.Endpoints.ContainsKey(_serviceEndpoint.Name)) { - _server?.Endpoints.Remove(_serviceEndpoint.Name); + throw new InvalidOperationException($"Duplicate callback proxy instance {Name} <{typeof(TInterface).Name}, {_serviceEndpoint.Contract.Name}>. Consider using a singleton callback proxy."); } + _server.Endpoints.Add(_serviceEndpoint.Name, _serviceEndpoint); } + } - public override string ToString() => Name; + public void Log(string message) => _logger.LogInformation(message); - public virtual bool Equals(IConnectionKey other) => EncryptAndSign == other.EncryptAndSign; + private void InitializeClientConnection(ClientConnection clientConnection) + { + _connection.Listen().LogException(_logger, Name); + clientConnection.Connection = _connection; + clientConnection.Server = _server; + _clientConnection = clientConnection; + } - public virtual ClientConnection CreateClientConnection(IConnectionKey key) => throw new NotImplementedException(); + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } - public class IpcProxy : DispatchProxy, IDisposable + protected virtual void Dispose(bool disposing) { - private static readonly MethodInfo InvokeMethod = typeof(IpcProxy).GetStaticMethod(nameof(GenericInvoke)); - private static readonly ConcurrentDictionaryWrapper InvokeByType = new(CreateDelegate); + _connectionLock.AssertDisposed(); + if (LogEnabled) + { + Log($"Dispose {Name}"); + } + if (disposing) + { + _server?.Endpoints.Remove(_serviceEndpoint.Name); + } + } - internal IServiceClient ServiceClient { get; set; } + public override string ToString() => Name; - public Connection Connection => ServiceClient.Connection; + public virtual bool Equals(IConnectionKey other) => SslServer == other.SslServer; - protected override object Invoke(MethodInfo targetMethod, object[] args) => GetInvoke(targetMethod)(ServiceClient, targetMethod.Name, args); + public virtual ClientConnection CreateClientConnection() => throw new NotImplementedException(); +} - public void Dispose() => ServiceClient.Dispose(); +public class IpcProxy : DispatchProxy, IDisposable +{ + private static readonly MethodInfo InvokeMethod = typeof(IpcProxy).GetStaticMethod(nameof(GenericInvoke)); + private static readonly ConcurrentDictionary InvokeByType = new(); - public void CloseConnection() => Connection?.Dispose(); + internal IServiceClient ServiceClient { get; set; } - private static InvokeDelegate GetInvoke(MethodInfo targetMethod) => InvokeByType.GetOrAdd(targetMethod.ReturnType); + public Connection Connection => ServiceClient.Connection; - private static InvokeDelegate CreateDelegate(Type taskType) - { - var resultType = taskType.IsGenericType ? taskType.GenericTypeArguments[0] : typeof(object); - return InvokeMethod.MakeGenericDelegate(resultType); - } - private static object GenericInvoke(IServiceClient serviceClient, string method, object[] args) => serviceClient.Invoke(method, args); - } + protected override object Invoke(MethodInfo targetMethod, object[] args) => GetInvoke(targetMethod)(ServiceClient, targetMethod, args); + + public void Dispose() => ServiceClient.Dispose(); + + public void CloseConnection() => Connection?.Dispose(); + + private static InvokeDelegate GetInvoke(MethodInfo targetMethod) => InvokeByType.GetOrAdd(targetMethod.ReturnType, taskType => + { + var resultType = taskType.IsGenericType ? taskType.GenericTypeArguments[0] : typeof(object); + return InvokeMethod.MakeGenericDelegate(resultType); + }); + + private static object GenericInvoke(IServiceClient serviceClient, MethodInfo method, object[] args) => serviceClient.Invoke(method, args); } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs b/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs index 1f714598..db59c9f7 100644 --- a/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs +++ b/src/UiPath.CoreIpc/Client/ServiceClientBuilder.cs @@ -1,102 +1,110 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Threading; -using System.Threading.Tasks; +namespace UiPath.CoreIpc; -namespace UiPath.CoreIpc +using ConnectionFactory = Func>; +using BeforeCallHandler = Func; + +public abstract class ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder { - using ConnectionFactory = Func>; - using BeforeCallHandler = Func; + 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; - public abstract class ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder + protected ServiceClientBuilder(Type callbackContract, IServiceProvider serviceProvider) { - 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 bool _encryptAndSign; + CallbackContract = callbackContract; + _serviceProvider = serviceProvider; + } - protected ServiceClientBuilder(Type callbackContract, IServiceProvider serviceProvider) - { - CallbackContract = callbackContract; - _serviceProvider = serviceProvider; - } + internal Type CallbackContract { get; } - internal Type CallbackContract { get; } + public TDerived DontReconnect() => ConnectionFactory((connection, _) => Task.FromResult(connection)); - public TDerived DontReconnect() => ConnectionFactory((connection, _) => Task.FromResult(connection)); + public TDerived ConnectionFactory(ConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + return (TDerived)this; + } - public TDerived ConnectionFactory(ConnectionFactory connectionFactory) + public TDerived EncryptAndSign(string certificateServerName) + { + if (string.IsNullOrWhiteSpace(certificateServerName)) { - _connectionFactory = connectionFactory; - return (TDerived)this; + throw new ArgumentException($"'{nameof(certificateServerName)}' must match the name on the server's certificate.", nameof(certificateServerName)); } + _sslServer = certificateServerName; + return (TDerived)this; + } - public TDerived EncryptAndSign() - { -#if WINDOWS - _encryptAndSign = true; -#endif - return (TDerived)this; - } + public TDerived BeforeCall(BeforeCallHandler beforeCall) + { + _beforeCall = beforeCall; + 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(ILogger logger) - { - _logger = logger; - return (TDerived)this; - } + public TDerived Logger(IServiceProvider serviceProvider) => Logger(serviceProvider.GetRequiredService>()); - public TDerived Logger(IServiceProvider serviceProvider) => Logger(serviceProvider.GetRequiredService>()); + public TDerived Serializer(ISerializer serializer) + { + _serializer = serializer; + return (TDerived) this; + } - 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 TDerived RequestTimeout(TimeSpan timeout) + public TInterface Build() + { + if (CallbackContract == null) { - _requestTimeout = timeout; - return (TDerived) this; + return BuildCore(null); } - - protected abstract TInterface BuildCore(EndpointSettings serviceEndpoint); - - public TInterface Build() + if (_logger == null) { - if (CallbackContract == null) - { - return BuildCore(null); - } - if (_logger == null) - { - Logger(_serviceProvider); - } - return BuildCore(new(CallbackContract, _callbackInstance) { Scheduler = _taskScheduler, ServiceProvider = _serviceProvider }); + Logger(_serviceProvider); } + return BuildCore(new(CallbackContract, _callbackInstance) { Scheduler = _taskScheduler, ServiceProvider = _serviceProvider }); } +} - public readonly struct CallInfo +public readonly struct CallInfo +{ + public CallInfo(bool newConnection, MethodInfo method, object[] arguments) { - public CallInfo(bool newConnection, string methodName, object[] arguments) - { - NewConnection = newConnection; - MethodName = methodName; - Arguments = arguments; - } - public bool NewConnection { get; } - public string MethodName { get; } - public object[] Arguments { get; } + 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/Connection.cs b/src/UiPath.CoreIpc/Connection.cs index 1817af63..e4404147 100644 --- a/src/UiPath.CoreIpc/Connection.cs +++ b/src/UiPath.CoreIpc/Connection.cs @@ -1,231 +1,359 @@ -using Microsoft.Extensions.Logging; -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; +using static TaskCompletionPool; +using static IOHelpers; +public sealed class Connection : IDisposable { - using RequestCompletionSource = TaskCompletionSource; - - public sealed class Connection : IDisposable - { - private readonly ConcurrentDictionary _requests = new(); - private long _requestCounter = -1; - private readonly int _maxMessageSize; - private readonly Lazy _receiveLoop; - private readonly AsyncLock _sendLock = new(); - - public Connection(Stream network, ISerializer serializer, ILogger logger, string name, int maxMessageSize = int.MaxValue) - { - Network = network; - Serializer = serializer; - Logger = logger; - Name = $"{name} {GetHashCode()}"; - _maxMessageSize = maxMessageSize; - _receiveLoop = new(ReceiveLoop); - } - public Stream Network { get; } - public ILogger Logger { get; internal set; } - public string Name { get; } - public ISerializer Serializer { get; } - public override string ToString() => Name; - public string NewRequestId() => Interlocked.Increment(ref _requestCounter).ToString(); - public Task Listen() => _receiveLoop.Value; - internal event Func RequestReceived; - internal event Action CancellationRequestReceived; - public event EventHandler Closed; - internal async Task RemoteCall(Request request, Stream uploadStream, CancellationToken token) - { - var requestBytes = await SerializeToStream(request); - var requestCompletion = new RequestCompletionSource(); - _requests[request.Id] = requestCompletion; - try - { - await SendRequest(requestBytes, uploadStream, token); - using (token.Register(CancelRequest)) - { - return await requestCompletion.Task; - } - } - finally - { - _requests.TryRemove(request.Id, out _); - } - void CancelRequest() - { - if (uploadStream == null) - { - CancelServerCall(request.Id).LogException(Logger, this); - } - else - { - Dispose(); - } - requestCompletion.TrySetCanceled(); - } - async Task CancelServerCall(string requestId) => - await SendMessage(MessageType.CancellationRequest, await SerializeToStream(new CancellationRequest(requestId)), default); + private static readonly IOException ClosedException = new("Connection closed."); + private readonly ConcurrentDictionary _requests = new(); + private long _requestCounter = -1; + private readonly int _maxMessageSize; + private readonly Lazy _receiveLoop; + private readonly SemaphoreSlim _sendLock = new(1); + private readonly WaitCallback _onResponse; + private readonly WaitCallback _onRequest; + private readonly WaitCallback _onCancellation; + 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) + { + Network = network; + _nestedStream = new NestedStream(network, 0); + Serializer = serializer; + Logger = logger; + Name = $"{name} {GetHashCode()}"; + _maxMessageSize = maxMessageSize; + _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 string NewRequestId() => Interlocked.Increment(ref _requestCounter).ToString(); + public Task Listen() => _receiveLoop.Value; + internal event Func RequestReceived; + internal event Action CancellationReceived; + public event EventHandler Closed; +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] +#endif + internal async ValueTask RemoteCall(Request request, CancellationToken token) + { + var requestCompletion = Rent(); + var requestId = request.Id; + _requests[requestId] = requestCompletion; + var tokenRegistration = token.UnsafeRegister(_cancelRequest, requestId); + try + { + await Send(request, token); } - private Task SendRequest(MemoryStream requestBytes, Stream uploadStream, CancellationToken cancellationToken) => uploadStream == null ? - SendMessage(MessageType.Request, requestBytes, cancellationToken) : - SendStream(new(MessageType.UploadRequest, requestBytes), uploadStream, cancellationToken); - internal async Task Send(Response response, CancellationToken cancellationToken) + catch { - var responseBytes = await SerializeToStream(response); - var downloadStream = response.DownloadStream; - if (downloadStream == null) - { - await SendMessage(MessageType.Response, responseBytes, cancellationToken); - return; - } - using (downloadStream) + tokenRegistration.Dispose(); + if (_requests.TryRemove(requestId, out _)) { - await SendStream(new(MessageType.DownloadResponse, responseBytes), downloadStream, cancellationToken); + requestCompletion.Return(); } + throw; } - private async Task SendStream(WireMessage message, Stream userStream, CancellationToken cancellationToken) + try { - using (await _sendLock.LockAsync(cancellationToken)) - { - using (cancellationToken.Register(Dispose)) - { - await Network.WriteMessage(message, cancellationToken); - await Network.WriteBuffer(BitConverter.GetBytes(userStream.Length), cancellationToken); - const int DefaultCopyBufferSize = 81920; - await userStream.CopyToAsync(Network, DefaultCopyBufferSize, cancellationToken); - } - } + return await requestCompletion.ValueTask(); } - - private Task SendMessage(MessageType messageType, MemoryStream data, CancellationToken cancellationToken) => - SendMessage(new(messageType, data), cancellationToken).WaitAsync(cancellationToken); - - private async Task SendMessage(WireMessage wireMessage, CancellationToken cancellationToken) + finally { - using (await _sendLock.LockAsync(cancellationToken)) - { - await Network.WriteMessage(wireMessage, CancellationToken.None); - } + _requests.TryRemove(requestId, out _); + tokenRegistration.Dispose(); + requestCompletion.Return(); } - - public void Dispose() + } + internal ValueTask Send(Request request, CancellationToken token) + { + Debug.Assert(request.Parameters == null || request.ObjectParameters == null); + var uploadStream = request.UploadStream; + var requestBytes = SerializeToStream(request); + return uploadStream == null ? + SendMessage(MessageType.Request, requestBytes, token) : + SendStream(MessageType.UploadRequest, requestBytes, uploadStream, token); + } + void CancelRequest(string requestId) + { + CancelServerCall(requestId).LogException(Logger, this); + if (_requests.TryRemove(requestId, out var requestCompletion)) { - var closedHandler = Interlocked.CompareExchange(ref Closed, null, Closed); - if (closedHandler == null) - { - return; - } - Network.Dispose(); - try - { - closedHandler.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) + requestCompletion.SetCanceled(); + } + return; + Task CancelServerCall(string requestId) => + SendMessage(MessageType.CancellationRequest, SerializeToStream(new CancellationRequest(requestId)), default).AsTask(); + } + 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) : + SendDownloadStream(responseBytes, response.DownloadStream, cancellationToken); + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private async ValueTask SendDownloadStream(MemoryStream responseBytes, Stream downloadStream, CancellationToken cancellationToken) + { + using (downloadStream) + { + await SendStream(MessageType.DownloadResponse, responseBytes, downloadStream, cancellationToken); + } + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private async ValueTask SendStream(MessageType messageType, Stream data, Stream userStream, CancellationToken cancellationToken) + { + await _sendLock.WaitAsync(cancellationToken); + CancellationTokenRegistration tokenRegistration = default; + try + { + 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; + await userStream.CopyToAsync(Network, DefaultCopyBufferSize, cancellationToken); + } + finally + { + _sendLock.Release(); + tokenRegistration.Dispose(); + } + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private async ValueTask SendMessage(MessageType messageType, MemoryStream data, CancellationToken cancellationToken) + { + await _sendLock.WaitAsync(cancellationToken); + try + { + await Network.WriteMessage(messageType, data, CancellationToken.None); + } + finally + { + _sendLock.Release(); + } + } + public void Dispose() + { + var closedHandler = Closed; + if (closedHandler == null || (closedHandler = Interlocked.CompareExchange(ref Closed, null, closedHandler)) == null) + { + return; + } + _sendLock.AssertDisposed(); + Network.Dispose(); + try + { + closedHandler.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + Logger.LogException(ex, this); + } + CompleteRequests(); + } + private void CompleteRequests() + { + foreach (var requestId in _requests.Keys) + { + if (_requests.TryRemove(requestId, out var requestCompletion)) { - Logger.LogException(ex, this); + requestCompletion.SetException(ClosedException); } - CompleteRequests(); } - - private void CompleteRequests() + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] +#endif + private async ValueTask ReadBuffer(int length) + { + int offset = 0; + int toRead = length; + do { - foreach (var completionSource in _requests.Values) + var read = await Network.ReadAsync( +#if NET461 + _buffer, offset, toRead); +#else + _buffer.AsMemory(offset, toRead)); +#endif + if (read == 0) { - completionSource.TrySetException(new IOException("Connection closed.")); + return false; } + offset += read; + toRead -= read; } - - private async Task ReceiveLoop() + while (toRead > 0); + return true; + } + private async Task ReceiveLoop() + { + try { - WireMessage message; - try + while (await ReadBuffer(HeaderLength)) { - while (!(message = await Network.ReadMessage(_maxMessageSize)).Empty) + Debug.Assert(SynchronizationContext.Current == null); + var length = BitConverter.ToInt32(_buffer, startIndex: 1); + if (length > _maxMessageSize) { - await HandleMessage(message); + throw new InvalidDataException($"Message too large. The maximum message size is {_maxMessageSize / (1024 * 1024)} megabytes."); } + _nestedStream.Reset(length); + await HandleMessage(); } - catch (Exception ex) - { - Logger.LogException(ex, $"{nameof(ReceiveLoop)} {Name}"); - } - Logger?.LogInformation($"{nameof(ReceiveLoop)} {Name} finished."); - Dispose(); - return; - async Task HandleMessage(WireMessage message) + } + catch (Exception ex) + { + Logger.LogException(ex, $"{nameof(ReceiveLoop)} {Name}"); + } + if (LogEnabled) + { + Log($"{nameof(ReceiveLoop)} {Name} finished."); + } + Dispose(); + return; +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + async ValueTask HandleMessage() + { + var messageType = (MessageType)_buffer[0]; + switch (messageType) { - var data = message.Data; - Action callback = null; - switch (message.MessageType) - { - case MessageType.Response: - callback = () => OnResponseReceived(data); - break; - case MessageType.Request when RequestReceived != null: - callback = () => OnRequestReceived(data); - break; - case MessageType.CancellationRequest when CancellationRequestReceived != null: - callback = () => CancellationRequestReceived(Deserialize(data).RequestId); - break; - case MessageType.UploadRequest: - await OnUploadRequest(data); - break; - case MessageType.DownloadResponse: - await OnDownloadResponse(data); - break; - default: - Logger?.LogInformation("Unknown message type " + message.MessageType); - break; - }; - if (callback != null) - { - Task.Run(callback).LogException(Logger, this); - } - } + case MessageType.Response: + RunAsync(_onResponse, await Deserialize()); + break; + case MessageType.Request: + RunAsync(_onRequest, await Deserialize()); + break; + case MessageType.CancellationRequest: + RunAsync(_onCancellation, (await Deserialize()).RequestId); + break; + case MessageType.UploadRequest: + await OnUploadRequest(); + return; + case MessageType.DownloadResponse: + await OnDownloadResponse(); + return; + default: + if (LogEnabled) + { + Log("Unknown message type " + messageType); + } + break; + }; } - - private async Task OnDownloadResponse(Stream data) + } + static void RunAsync(WaitCallback callback, object state) => ThreadPool.UnsafeQueueUserWorkItem(callback, state); + private async Task OnDownloadResponse() + { + var response = await Deserialize(); + await EnterStreamMode(); + var streamDisposed = new TaskCompletionSource(); + EventHandler disposedHandler = delegate { streamDisposed.TrySetResult(true); }; + _nestedStream.Disposed += disposedHandler; + try { - var downloadStream = await WrapNetworkStream(); - var streamDisposed = new TaskCompletionSource(); - downloadStream.Disposed += delegate { streamDisposed.TrySetResult(true); }; - OnResponseReceived(data, downloadStream); + response.DownloadStream = _nestedStream; + OnResponseReceived(response); await streamDisposed.Task; } - - private async Task OnUploadRequest(Stream data) + finally { - using var uploadStream = await WrapNetworkStream(); - await OnRequestReceived(data, uploadStream); + _nestedStream.Disposed -= disposedHandler; } - - private async Task WrapNetworkStream() + } + private async Task OnUploadRequest() + { + var request = await Deserialize(); + await EnterStreamMode(); + using (_nestedStream) { - var lengthBytes = await Network.ReadBuffer(sizeof(long), default); - var userStreamLength = BitConverter.ToInt64(lengthBytes, 0); - return new NestedStream(Network, userStreamLength); + request.UploadStream = _nestedStream; + await OnRequestReceived(request); } - - private async Task SerializeToStream(object value) + } + private async Task EnterStreamMode() + { + if (!await ReadBuffer(sizeof(long))) + { + throw ClosedException; + } + var userStreamLength = BitConverter.ToInt64(_buffer, startIndex: 0); + _nestedStream.Reset(userStreamLength); + } + private MemoryStream SerializeToStream(object value) + { + var stream = GetStream(); + try { - var stream = new MemoryStream(); - await Serializer.Serialize(value, stream); + stream.Position = HeaderLength; + Serializer.Serialize(value, stream); return stream; } - private T Deserialize(Stream data) => Serializer.Deserialize(data); - private Task OnRequestReceived(Stream data, Stream uploadStream = null) => RequestReceived(Deserialize(data), uploadStream); - private void OnResponseReceived(Stream data, Stream downloadStream = null) + catch { - var response = Deserialize(data); - response.DownloadStream = downloadStream; - Logger?.LogInformation($"Received response for request {response.RequestId} {Name}."); - if (_requests.TryGetValue(response.RequestId, out var completionSource)) + stream.Dispose(); + throw; + } + } + private ValueTask Deserialize() => Serializer.DeserializeAsync(_nestedStream); + private void OnCancellationReceived(string requestId) + { + try + { + CancellationReceived(requestId); + } + catch(Exception ex) + { + Log(ex); + } + } + private void Log(Exception ex) => Logger.LogException(ex, Name); + private ValueTask OnRequestReceived(Request request) + { + try + { + return RequestReceived(request); + } + catch (Exception ex) + { + Log(ex); + } + return default; + } + private void OnResponseReceived(Response response) + { + try + { + if (LogEnabled) + { + Log($"Received response for request {response.RequestId} {Name}."); + } + if (_requests.TryRemove(response.RequestId, out var completionSource)) { - completionSource.TrySetResult(response); + completionSource.SetResult(response); } } + catch (Exception ex) + { + Log(ex); + } } + public void Log(string message) => Logger.LogInformation(message); } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Dtos.cs b/src/UiPath.CoreIpc/Dtos.cs index 758844c4..cbf278ba 100644 --- a/src/UiPath.CoreIpc/Dtos.cs +++ b/src/UiPath.CoreIpc/Dtos.cs @@ -1,137 +1,84 @@ -using System; -using System.IO; -using System.Text; +using System.Text; using Newtonsoft.Json; -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; +public class Message { - public class Message - { - [JsonIgnore] - internal EndpointSettings Endpoint { get; set; } - [JsonIgnore] - public IClient Client { get; set; } - [JsonIgnore] - public TimeSpan RequestTimeout { get; set; } - public TCallbackInterface GetCallback() where TCallbackInterface : class => Client.GetCallback(Endpoint); - public void ImpersonateClient(Action action) => Client.Impersonate(action); - } - public class Message : Message - { - public Message(TPayload payload) => Payload = payload; - public TPayload Payload { get; } - } - class Request + 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); + public void ImpersonateClient(Action action) => Client.Impersonate(action); +} +public class Message : Message +{ + public Message(TPayload payload) => Payload = payload; + public TPayload Payload { get; } +} +record Request(string Endpoint, string Id, string MethodName, string[] Parameters, object[] ObjectParameters, double TimeoutInSeconds) +{ + internal 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); +} +record CancellationRequest(string RequestId); +record Response(string RequestId, string Data = null, object ObjectData = null, Error Error = null) +{ + internal 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 Request(string endpoint, string id, string methodName, string[] parameters, double timeoutInSeconds) + if (Error != null) { - Endpoint = endpoint; - Id = id; - MethodName = methodName; - Parameters = parameters; - TimeoutInSeconds = timeoutInSeconds; + throw new RemoteException(Error); } - public double TimeoutInSeconds { get; } - public string Endpoint { get; } - public string Id { get; } - public string MethodName { get; } - public string[] Parameters { get; } - public override string ToString() => $"{Endpoint} {MethodName} {Id}."; - internal TimeSpan GetTimeout(TimeSpan defaultTimeout) => TimeoutInSeconds == 0 ? defaultTimeout : TimeSpan.FromSeconds(TimeoutInSeconds); + return (TResult)(DownloadStream ?? (objectParameters ? + serializer.Deserialize(ObjectData, typeof(TResult)) : serializer.Deserialize(Data ?? "", typeof(TResult)))); } - class CancellationRequest +} +[Serializable] +public record Error(string Message, string StackTrace, string Type, Error InnerError) +{ + public override string ToString() => new RemoteException(this).ToString(); +} +[Serializable] +public class RemoteException : Exception +{ + public RemoteException(Error error) : base(error.Message, error.InnerError == null ? null : new RemoteException(error.InnerError)) { - public CancellationRequest(string requestId) => RequestId = requestId; - public string RequestId { get; } + Type = error.Type; + StackTrace = error.StackTrace; } - class Response + public string Type { get; } + public override string StackTrace { get; } + public new RemoteException InnerException => (RemoteException)base.InnerException; + public override string ToString() { - [JsonConstructor] - private Response(string requestId, string data, Error error) - { - RequestId = requestId; - Data = data; - Error = error; - } - public string RequestId { get; } - public string Data { get; } - public Error Error { get; } - [JsonIgnore] - public Stream DownloadStream { get; set; } - public static Response Fail(Request request, string message) => Fail(request, new Exception(message)); - public static Response Fail(Request request, Exception ex) => new(request.Id, null, new(ex)); - public static Response Success(Request request, string data) => new(request.Id, data, null); - public static Response Success(Request request, Stream downloadStream) => new(request.Id, null, null) { DownloadStream = downloadStream }; - public Response CheckError() => Error == null ? this : throw new RemoteException(Error); + var result = new StringBuilder(); + GatherInnerExceptions(result); + return result.ToString(); } - [Serializable] - public class Error + private void GatherInnerExceptions(StringBuilder result) { - [JsonConstructor] - private Error(string message, string stackTrace, string type, Error innerError) + result.Append($"{nameof(RemoteException)} wrapping {Type}: {Message} "); + if (InnerException == null) { - Message = message; - StackTrace = stackTrace; - Type = type; - InnerError = innerError; + result.Append("\n"); } - public Error(Exception ex) : this(ex.Message, ex.StackTrace ?? ex.GetBaseException().StackTrace, GetExceptionType(ex), - ex.InnerException == null ? null : new(ex.InnerException)) - { - } - public string Message { get; } - public string StackTrace { get; } - public string Type { get; } - public Error InnerError { get; } - public override string ToString() => new RemoteException(this).ToString(); - private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName; - } - [Serializable] - public class RemoteException : Exception - { - public RemoteException(Error error) : base(error.Message, error.InnerError == null ? null : new RemoteException(error.InnerError)) - { - Type = error.Type; - StackTrace = error.StackTrace; - } - public string Type { get; } - public override string StackTrace { get; } - public new RemoteException InnerException => (RemoteException)base.InnerException; - public override string ToString() - { - var result = new StringBuilder(); - GatherInnerExceptions(result); - return result.ToString(); - } - private void GatherInnerExceptions(StringBuilder result) - { - result.Append($"{nameof(RemoteException)} wrapping {Type}: {Message} "); - if (InnerException == null) - { - result.Append("\n"); - } - else - { - result.Append(" ---> "); - InnerException.GatherInnerExceptions(result); - result.Append("\n\t--- End of inner exception stack trace ---\n"); - } - result.Append(StackTrace); - } - public bool Is() where TException : Exception => Type == typeof(TException).FullName; - } - enum MessageType : byte { Request, Response, CancellationRequest, UploadRequest, DownloadResponse } - readonly struct WireMessage - { - public WireMessage(MessageType messageType, MemoryStream data) + else { - MessageType = messageType; - Data = data; - data.Position = 0; + result.Append(" ---> "); + InnerException.GatherInnerExceptions(result); + result.Append("\n\t--- End of inner exception stack trace ---\n"); } - public MessageType MessageType { get; } - public MemoryStream Data { get; } - public bool Empty => Data == null; - public int Length => (int)Data.Length; - public byte[] Buffer => Data.GetBuffer(); + result.Append(StackTrace); } -} \ No newline at end of file + public bool Is() where TException : Exception => Type == typeof(TException).FullName; +} +enum MessageType : byte { Request, Response, CancellationRequest, UploadRequest, DownloadResponse } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/GlobalSuppressions.cs b/src/UiPath.CoreIpc/GlobalSuppressions.cs new file mode 100644 index 00000000..cee98987 --- /dev/null +++ b/src/UiPath.CoreIpc/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +using System.Diagnostics.CodeAnalysis; +[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/Helpers.cs b/src/UiPath.CoreIpc/Helpers.cs index 313fd8fd..58c39029 100644 --- a/src/UiPath.CoreIpc/Helpers.cs +++ b/src/UiPath.CoreIpc/Helpers.cs @@ -1,331 +1,312 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using Microsoft.IO; using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; using System.IO.Pipes; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; +using System.Net; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; +using static CancellationTokenSourcePool; +public static class Helpers { - public 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); + [Conditional("DEBUG")] + public static void AssertDisposed(this SemaphoreSlim semaphore) => semaphore.AssertFieldNull("m_waitHandle"); + [Conditional("DEBUG")] + public static void AssertDisposed(this CancellationTokenSource cts) + { +#if NET461 + cts.AssertFieldNull("m_kernelEvent"); + cts.AssertFieldNull("m_timer"); +#else + cts.AssertFieldNull("_kernelEvent"); + cts.AssertFieldNull("_timer"); +#endif + } + [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 => + (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) { - public const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly; - public 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) + 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) { - 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) - { - throw new ArgumentOutOfRangeException(nameof(name), name, "Generic methods are not supported " + method); - } - return method; + throw new ArgumentOutOfRangeException(nameof(name), name, "Generic methods are not supported " + method); } - public static IEnumerable GetInterfaceMethods(this Type type) => - type.GetMethods().Concat(type.GetInterfaces().SelectMany(i => i.GetMethods())); - public 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) + return method; + } + public static IEnumerable GetInterfaceMethods(this Type type) => + type.GetMethods().Concat(type.GetInterfaces().SelectMany(i => i.GetMethods())); + public 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) + { + var message = $"{tag} # {ex}"; + if (logger != null) { - var message = $"{tag} # {ex}"; - if (logger != null) - { - logger.LogError(message); - } - else - { - Trace.TraceError(message); - } + logger.LogError(message); } - public static void LogException(this Task task, ILogger logger, object tag) => task.ContinueWith(result => logger.LogException(result.Exception, tag), TaskContinuationOptions.NotOnRanToCompletion); - public static async Task Timeout(this TimeSpan timeout, List cancellationTokens, - Func> func, string message, Func exceptionHandler) + else { - var timeoutCancellationSource = new CancellationTokenSource(timeout); - cancellationTokens.Add(timeoutCancellationSource.Token); - var linkedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokens.ToArray()); - try - { - return await func(linkedCancellationSource.Token); - } - catch (Exception ex) - { - var exception = ex; - if (timeoutCancellationSource.IsCancellationRequested) - { - exception = new TimeoutException(message + " timed out.", ex); - } - Dispose(); - await exceptionHandler(exception); - } - finally - { - Dispose(); - } - return default; - void Dispose() - { - timeoutCancellationSource.Dispose(); - linkedCancellationSource.Dispose(); - } + Trace.TraceError(message); } } - public static class IOHelpers + 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 +{ + const int MaxBytes = 100 * 1024 * 1024; + 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()); -#elif NET5_0_WINDOWS - return NamedPipeServerStreamAcl.Create(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options, inBufferSize: 0, outBufferSize: 0, pipeSecurity()); + return new(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options, inBufferSize: 0, outBufferSize: 0, pipeSecurity()); +#elif WINDOWS + return NamedPipeServerStreamAcl.Create(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options, inBufferSize: 0, outBufferSize: 0, pipeSecurity()); #else - return new(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options); + return new(pipeName, direction, maxNumberOfServerInstances, transmissionMode, options); #endif - } + } - public static PipeSecurity LocalOnly(this PipeSecurity pipeSecurity) => pipeSecurity.Deny(WellKnownSidType.NetworkSid, PipeAccessRights.FullControl); + public static PipeSecurity LocalOnly(this PipeSecurity pipeSecurity) => pipeSecurity.Deny(WellKnownSidType.NetworkSid, PipeAccessRights.FullControl); - public static PipeSecurity Deny(this PipeSecurity pipeSecurity, WellKnownSidType sid, PipeAccessRights pipeAccessRights) => - pipeSecurity.Deny(new SecurityIdentifier(sid, null), pipeAccessRights); + public static PipeSecurity Deny(this PipeSecurity pipeSecurity, WellKnownSidType sid, PipeAccessRights pipeAccessRights) => + pipeSecurity.Deny(new SecurityIdentifier(sid, null), pipeAccessRights); - public static PipeSecurity Deny(this PipeSecurity pipeSecurity, IdentityReference sid, PipeAccessRights pipeAccessRights) - { - pipeSecurity.SetAccessRule(new(sid, pipeAccessRights, AccessControlType.Deny)); - return pipeSecurity; - } + public static PipeSecurity Deny(this PipeSecurity pipeSecurity, IdentityReference sid, PipeAccessRights pipeAccessRights) + { + pipeSecurity.SetAccessRule(new(sid, pipeAccessRights, AccessControlType.Deny)); + return pipeSecurity; + } - public static PipeSecurity Allow(this PipeSecurity pipeSecurity, WellKnownSidType sid, PipeAccessRights pipeAccessRights) => - pipeSecurity.Allow(new SecurityIdentifier(sid, null), pipeAccessRights); + public static PipeSecurity Allow(this PipeSecurity pipeSecurity, WellKnownSidType sid, PipeAccessRights pipeAccessRights) => + pipeSecurity.Allow(new SecurityIdentifier(sid, null), pipeAccessRights); - public static PipeSecurity Allow(this PipeSecurity pipeSecurity, IdentityReference sid, PipeAccessRights pipeAccessRights) - { - pipeSecurity.SetAccessRule(new(sid, pipeAccessRights, AccessControlType.Allow)); - return pipeSecurity; - } + public static PipeSecurity Allow(this PipeSecurity pipeSecurity, IdentityReference sid, PipeAccessRights pipeAccessRights) + { + pipeSecurity.SetAccessRule(new(sid, pipeAccessRights, AccessControlType.Allow)); + return pipeSecurity; + } - public static PipeSecurity AllowCurrentUser(this PipeSecurity pipeSecurity, bool onlyNonAdmin = false) + 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)) { - if (onlyNonAdmin && new WindowsPrincipal(currentIdentity).IsInRole(WindowsBuiltInRole.Administrator)) - { - return pipeSecurity; - } - pipeSecurity.Allow(currentIdentity.User, PipeAccessRights.ReadWrite|PipeAccessRights.CreateNewInstance); + return pipeSecurity; } - return pipeSecurity; + pipeSecurity.Allow(currentIdentity.User, PipeAccessRights.ReadWrite|PipeAccessRights.CreateNewInstance); } + return pipeSecurity; + } - public static bool PipeExists(string pipeName, int timeout = 1) + public static bool PipeExists(string pipeName, int timeout = 1) + { + try { - try - { - using (var client = new NamedPipeClientStream(pipeName)) - { - client.Connect(timeout); - } - return true; - } - catch (Exception ex) + using (var client = new NamedPipeClientStream(pipeName)) { - Trace.WriteLine(ex.ToString()); + client.Connect(timeout); } - return false; + return true; + } + catch (Exception ex) + { + Trace.WriteLine(ex.ToString()); } + return false; + } - internal static async Task WriteMessage(this Stream stream, WireMessage message, CancellationToken cancellationToken = default) + internal static ValueTask WriteMessage(this Stream stream, MessageType messageType, Stream data, CancellationToken cancellationToken = default) + { + var recyclableStream = (RecyclableMemoryStream)data; + recyclableStream.Position = 0; + var buffer = recyclableStream.GetSpan(HeaderLength); + var totalLength = (int)recyclableStream.Length; + buffer[0] = (byte)messageType; + var payloadLength = totalLength - HeaderLength; + // https://github.com/dotnet/runtime/blob/85441ce69b81dfd5bf57b9d00ba525440b7bb25d/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs#L133 + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer.Slice(1)), payloadLength); + return stream.WriteMessageCore(recyclableStream, cancellationToken); + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private static async ValueTask WriteMessageCore(this Stream stream, RecyclableMemoryStream recyclableStream, CancellationToken cancellationToken) + { + using (recyclableStream) { - var header = new byte[sizeof(int) + 1]; - header[0] = (byte)message.MessageType; - // https://github.com/dotnet/runtime/blob/85441ce69b81dfd5bf57b9d00ba525440b7bb25d/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs#L133 - Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(header.AsSpan(1)), message.Length); - await stream.WriteBuffer(header, cancellationToken); - await stream.WriteAsync(message.Buffer, 0, message.Length, cancellationToken); + await recyclableStream.CopyToAsync(stream, 0, cancellationToken); } - internal static Task WriteBuffer(this Stream stream, byte[] buffer, CancellationToken cancellationToken) => - stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + internal static Task WriteBuffer(this Stream stream, byte[] buffer, CancellationToken cancellationToken) => + stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); +} +public static class Validator +{ + public static void Validate(ServiceHostBuilder serviceHostBuilder) + { + foreach (var endpointSettings in serviceHostBuilder.Endpoints.Values) + { + endpointSettings.Validate(); + } + } - internal static async Task ReadMessage(this Stream stream, int maxMessageSize, CancellationToken cancellationToken = default) + public static void Validate(ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder + => Validate(typeof(TInterface), builder.CallbackContract); + + public static void Validate(params Type[] contracts) + { + foreach (var contract in contracts.Where(c => c != null)) { - var header = await stream.ReadBufferCore(sizeof(int)+1, cancellationToken); - if (header.Length == 0) - { - return new(); - } - var messageType = (MessageType)header[0]; - var length = BitConverter.ToInt32(header, 1); - if(length > maxMessageSize) + if (!contract.IsInterface) { - throw new InvalidDataException($"Message too large. The maximum message size is {maxMessageSize/(1024*1024)} megabytes."); + throw new ArgumentOutOfRangeException(nameof(contract), "The contract must be an interface! " + contract); } - var data = await stream.ReadBufferCore(length, cancellationToken); - if (data.Length == 0) + foreach (var method in contract.GetInterfaceMethods()) { - return new(); + Validate(method); } - return new(messageType, new MemoryStream(data)); } + } - internal static async Task ReadBuffer(this Stream stream, int length, CancellationToken cancellationToken) + private static void Validate(MethodInfo method) + { + var returnType = method.ReturnType; + CheckMethod(); + var parameters = method.GetParameters(); + int streamCount = 0; + for (int index = 0; index < parameters.Length; index++) { - var messageData = await stream.ReadBufferCore(length, cancellationToken); - if (messageData.Length == 0) + var parameter = parameters[index]; + CheckMessageParameter(index, parameter); + CheckCancellationToken(index, parameter); + if (parameter.ParameterType == typeof(Stream)) { - throw new IOException("Connection closed."); + CheckStreamParameter(); } - return messageData; - } - - private static async Task ReadBufferCore(this Stream stream, int length, CancellationToken cancellationToken) - { - var bytes = new byte[length]; - int offset = 0; - int remaining = length; - while(remaining > 0) + else { - var read = await stream.ReadAsync(bytes, offset, remaining, cancellationToken); - if(read == 0) - { - return Array.Empty(); - } - remaining -= read; - offset += read; + CheckDerivedStream(method, parameter.ParameterType); } - return bytes; } - public static T Deserialize(this ISerializer serializer, Stream binary) => (T)serializer.Deserialize(binary, typeof(T)); - public static T Deserialize(this ISerializer serializer, string json) => (T)serializer.Deserialize(json, typeof(T)); - } - public static class Validator - { - public static void Validate(ServiceHostBuilder serviceHostBuilder) + void CheckStreamParameter() { - foreach (var endpointSettings in serviceHostBuilder.Endpoints.Values) + streamCount++; + if (streamCount > 1) { - endpointSettings.Validate(); + throw new ArgumentException($"Only one Stream parameter is allowed! {method}"); } - } - - public static void Validate(ServiceClientBuilder builder) where TInterface : class where TDerived : ServiceClientBuilder - => Validate(typeof(TInterface), builder.CallbackContract); - - public static void Validate(params Type[] contracts) - { - foreach (var contract in contracts.Where(c => c != null)) + if (!method.ReturnType.IsGenericType) { - if (!contract.IsInterface) - { - throw new ArgumentOutOfRangeException(nameof(contract), "The contract must be an interface! " + contract); - } - foreach (var method in contract.GetInterfaceMethods()) - { - Validate(method); - } + throw new ArgumentException($"Upload methods must return a value! {method}"); } } - - private static void Validate(MethodInfo method) + void CheckMethod() { - var returnType = method.ReturnType; - CheckMethod(); - var parameters = method.GetParameters(); - int streamCount = 0; - for (int index = 0; index < parameters.Length; index++) - { - var parameter = parameters[index]; - CheckMessageParameter(index, parameter); - CheckCancellationToken(index, parameter); - if (parameter.ParameterType == typeof(Stream)) - { - CheckStreamParameter(); - } - else - { - CheckDerivedStream(method, parameter.ParameterType); - } - } - void CheckStreamParameter() + if (!typeof(Task).IsAssignableFrom(returnType)) { - streamCount++; - if (streamCount > 1) - { - throw new ArgumentException($"Only one Stream parameter is allowed! {method}"); - } - if (!method.ReturnType.IsGenericType) - { - throw new ArgumentException($"Upload methods must return a value! {method}"); - } + throw new ArgumentException($"Method does not return Task! {method}"); } - void CheckMethod() + if (returnType.IsGenericType) { - if (!typeof(Task).IsAssignableFrom(returnType)) + var returnValueType = returnType.GenericTypeArguments[0]; + if (returnValueType != typeof(Stream)) { - throw new ArgumentException($"Method does not return Task! {method}"); - } - if (returnType.IsGenericType) - { - var returnValueType = returnType.GenericTypeArguments[0]; - if (returnValueType != typeof(Stream)) - { - CheckDerivedStream(method, returnValueType); - } + CheckDerivedStream(method, returnValueType); } } - void CheckMessageParameter(int index, ParameterInfo parameter) + } + void CheckMessageParameter(int index, ParameterInfo parameter) + { + if (typeof(Message).IsAssignableFrom(parameter.ParameterType) && index != parameters.Length - 1 && + parameters[parameters.Length - 1].ParameterType != typeof(CancellationToken)) { - if (typeof(Message).IsAssignableFrom(parameter.ParameterType) && index != parameters.Length - 1 && - parameters[parameters.Length - 1].ParameterType != typeof(CancellationToken)) - { - throw new ArgumentException($"The message must be the last parameter before the cancellation token! {method}"); - } + throw new ArgumentException($"The message must be the last parameter before the cancellation token! {method}"); } - void CheckCancellationToken(int index, ParameterInfo parameter) + } + void CheckCancellationToken(int index, ParameterInfo parameter) + { + if (parameter.ParameterType == typeof(CancellationToken) && index != parameters.Length - 1) { - if (parameter.ParameterType == typeof(CancellationToken) && index != parameters.Length - 1) - { - throw new ArgumentException($"The CancellationToken parameter must be the last! {method}"); - } + throw new ArgumentException($"The CancellationToken parameter must be the last! {method}"); } } + } - private static void CheckDerivedStream(MethodInfo method, Type type) + private static void CheckDerivedStream(MethodInfo method, Type type) + { + if (typeof(Stream).IsAssignableFrom(type)) { - if (typeof(Stream).IsAssignableFrom(type)) + throw new ArgumentException($"Stream parameters must be typed as Stream! {method}"); + } + } +} +public 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); + } + public Exception CheckTimeout(Exception exception, string message) + { + if (_timeoutCancellationSource.IsCancellationRequested) + { + if (!_cancellationToken.IsCancellationRequested) { - throw new ArgumentException($"Stream parameters must be typed as Stream! {method}"); + return new TimeoutException(message + " timed out.", exception); + } + if (exception is not TaskCanceledException) + { + return new TaskCanceledException(message, exception); } } + return exception; } - public readonly struct ConcurrentDictionaryWrapper + public void ThrowTimeout(Exception exception, string message) { - private readonly ConcurrentDictionary _dictionary; - private readonly Func _valueFactory; - public ConcurrentDictionaryWrapper(Func valueFactory, int capacity = 31) + var newException = CheckTimeout(exception, message); + if (newException != exception) { - _dictionary = new(Environment.ProcessorCount, capacity); - _valueFactory = key => valueFactory(key); + throw newException; } - public TValue GetOrAdd(TKey key) => _dictionary.GetOrAdd(key, _valueFactory); - public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); - public bool TryRemove(TKey key, out TValue value) => _dictionary.TryRemove(key, out value); } + public void Dispose() + { + _linkedRegistration.Dispose(); + _timeoutCancellationSource.Return(); + } + public CancellationToken Token => _timeoutCancellationSource.Token; } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/IpcJsonSerializer.cs b/src/UiPath.CoreIpc/IpcJsonSerializer.cs index 69a3e5ea..33a9faf5 100644 --- a/src/UiPath.CoreIpc/IpcJsonSerializer.cs +++ b/src/UiPath.CoreIpc/IpcJsonSerializer.cs @@ -1,33 +1,59 @@ using Newtonsoft.Json; -using System; -using System.IO; -using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using System.Buffers; +using System.Globalization; +using System.Text; +namespace UiPath.CoreIpc; -namespace UiPath.CoreIpc +public interface ISerializer { - 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) { - object Deserialize(Stream json, Type type); - object Deserialize(string json, Type type); - string Serialize(object obj); - Task Serialize(object obj, Stream stream); + var stringWriter = new StringWriter(new StringBuilder(capacity: 256), CultureInfo.InvariantCulture); + Serialize(obj, stringWriter, StringArgsSerializer); + return stringWriter.ToString(); } - class IpcJsonSerializer : ISerializer + public object Deserialize(string json, Type type) { - public object Deserialize(string json, Type type) => JsonConvert.DeserializeObject(json, type); - public object Deserialize(Stream json, Type type) - { - var serializer = JsonSerializer.CreateDefault(); - var streamReader = new StreamReader(json); - return serializer.Deserialize(streamReader, type); - } - public string Serialize(object obj) => JsonConvert.SerializeObject(obj); - public async Task Serialize(object obj, Stream stream) - { - var serializer = JsonSerializer.CreateDefault(); - var streamWriter = new StreamWriter(stream); - serializer.Serialize(streamWriter, obj); - await streamWriter.FlushAsync(); - } + 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/NamedPipe/NamedPipeClient.cs b/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs index 0416b537..61584f39 100644 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs +++ b/src/UiPath.CoreIpc/NamedPipe/NamedPipeClient.cs @@ -1,58 +1,51 @@ using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; -using System; using System.Security.Principal; -using Microsoft.Extensions.Logging; -using System.IO; -namespace UiPath.CoreIpc.NamedPipe +namespace UiPath.CoreIpc.NamedPipe; + +using ConnectionFactory = Func>; +using BeforeCallHandler = Func; + +interface INamedPipeKey : IConnectionKey { - using ConnectionFactory = Func>; - using BeforeCallHandler = Func; + string ServerName { get; } + string PipeName { get; } + bool AllowImpersonation { get; } +} - interface INamedPipeKey : IConnectionKey +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) { - string ServerName { get; } - string PipeName { get; } - bool AllowImpersonation { get; } + ServerName = serverName; + PipeName = pipeName; + AllowImpersonation = allowImpersonation; + HashCode = (serverName, pipeName, allowImpersonation, sslServer).GetHashCode(); } - - class NamedPipeClient : ServiceClient, INamedPipeKey where TInterface : class + 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 readonly int _hashCode; - public NamedPipeClient(string serverName, string pipeName, ISerializer serializer, TimeSpan requestTimeout, bool allowImpersonation, ILogger logger, ConnectionFactory connectionFactory, bool encryptAndSign, BeforeCallHandler beforeCall, EndpointSettings serviceEndpoint) : base(serializer, requestTimeout, logger, connectionFactory, encryptAndSign, beforeCall, serviceEndpoint) + private NamedPipeClientStream _pipe; + public NamedPipeClientConnection(IConnectionKey connectionKey) : base(connectionKey) { } + public override bool Connected => _pipe?.IsConnected is true; + protected override void Dispose(bool disposing) { - ServerName = serverName; - PipeName = pipeName; - AllowImpersonation = allowImpersonation; - _hashCode = (serverName, pipeName, allowImpersonation, encryptAndSign).GetHashCode(); + _pipe?.Dispose(); + base.Dispose(disposing); } - public override string Name => base.Name ?? PipeName; - public string ServerName { get; } - public string PipeName { get; } - public bool AllowImpersonation { get; } - public override int GetHashCode() => _hashCode; - 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(IConnectionKey key) => new NamedPipeClientConnection(key); - class NamedPipeClientConnection : ClientConnection + public override async Task Connect(CancellationToken cancellationToken) { - 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 Stream Network => _pipe; - public override Task Connect(CancellationToken cancellationToken) - { - var key = (INamedPipeKey)ConnectionKey; - _pipe = new(key.ServerName, key.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous, key.AllowImpersonation ? TokenImpersonationLevel.Impersonation : TokenImpersonationLevel.Identification); - return _pipe.ConnectAsync(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 index b68e9cdd..5e4b69d4 100644 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs +++ b/src/UiPath.CoreIpc/NamedPipe/NamedPipeClientBuilder.cs @@ -1,57 +1,53 @@ -using System; -using System.Threading.Tasks; +namespace UiPath.CoreIpc.NamedPipe; -namespace UiPath.CoreIpc.NamedPipe +public abstract class NamedPipeClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder { - 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() { - 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, _encryptAndSign, _beforeCall, serviceEndpoint).CreateProxy(); + _allowImpersonation = true; + return this as TDerived; } - public class NamedPipeClientBuilder : NamedPipeClientBuilderBase, TInterface> where TInterface : class + 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) { - public NamedPipeClientBuilder(string pipeName) : base(pipeName){} + _callbackInstance = singleton; + return this; } - public class NamedPipeClientBuilder : NamedPipeClientBuilderBase, TInterface> where TInterface : class where TCallbackInterface : class + public NamedPipeClientBuilder TaskScheduler(TaskScheduler taskScheduler) { - 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; - } + _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 index f503d3aa..5f408757 100644 --- a/src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs +++ b/src/UiPath.CoreIpc/NamedPipe/NamedPipeListener.cs @@ -1,64 +1,55 @@ -using System; -using System.IO; -using System.IO.Pipes; +using System.IO.Pipes; using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.NamedPipe +namespace UiPath.CoreIpc.NamedPipe; + +public class NamedPipeSettings : ListenerSettings { - public class NamedPipeSettings : ListenerSettings - { - public NamedPipeSettings(string pipeName) : base(pipeName) { } - public Action AccessControl { get; set; } - } - class NamedPipeListener : Listener + 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 { - 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) { - 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) - { - // on linux WaitForConnectionAsync has to be cancelled with Dispose - using (cancellationToken.Register(Dispose)) - { - await _server.WaitForConnectionAsync(); - } - } - protected override Stream Network => _server; - public override void Impersonate(Action action) => _server.RunAsClient(()=>action()); - protected override void Dispose(bool disposing) - { - _server.Dispose(); - base.Dispose(disposing); - } - PipeSecurity GetPipeSecurity() + _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) { - 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); + 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)); - } +} +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/NestedStream.cs b/src/UiPath.CoreIpc/NestedStream.cs index d71f2770..fdc40877 100644 --- a/src/UiPath.CoreIpc/NestedStream.cs +++ b/src/UiPath.CoreIpc/NestedStream.cs @@ -1,138 +1,156 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; + +/// +/// 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 { /// - /// 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 + /// The stream to read from. /// - public class NestedStream : Stream + private Stream _underlyingStream; + /// + /// The total length of the stream. + /// + private long _length; + /// + /// The remaining bytes allowed to be read. + /// + private long _remainingBytes; + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + /// The number of bytes to read from the parent stream. + public NestedStream(Stream underlyingStream, long length) + { + _underlyingStream = underlyingStream; + Reset(length); + } + + public void Reset(long length) { - /// - /// The stream to read from. - /// - private Stream _underlyingStream; - /// - /// The total length of the stream. - /// - private readonly long _length; - /// - /// The remaining bytes allowed to be read. - /// - private long _remainingBytes; - /// - /// Initializes a new instance of the class. - /// - /// The stream to read from. - /// The number of bytes to read from the parent stream. - public NestedStream(Stream underlyingStream, long length) + _remainingBytes = length; + _length = length; + } + + public event EventHandler Disposed; + /// + public bool IsDisposed => _underlyingStream == null; + /// + public override bool CanRead => !IsDisposed; + /// + public override bool CanSeek => !IsDisposed && _underlyingStream.CanSeek; + /// + public override bool CanWrite => false; + /// + public override long Length => _length; + /// + public override long Position + { + get => _length - _remainingBytes; + set => Seek(value, SeekOrigin.Begin); + } + /// + public override void Flush() => throw new NotSupportedException(); + /// + public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); + /// + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if ((count = BytesToRead(buffer, offset, count)) <= 0) { - _underlyingStream = underlyingStream; - _remainingBytes = length; - _length = length; + return 0; } - public event EventHandler Disposed; - /// - public bool IsDisposed => _underlyingStream == null; - /// - public override bool CanRead => !IsDisposed; - /// - public override bool CanSeek => !IsDisposed && _underlyingStream.CanSeek; - /// - public override bool CanWrite => false; - /// - public override long Length => _underlyingStream.CanSeek ? _length : throw new NotSupportedException(); - /// - public override long Position + int bytesRead = await _underlyingStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + _remainingBytes -= bytesRead; + return bytesRead; + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public async override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (buffer.Length > _remainingBytes) { - get => _length - _remainingBytes; - set => Seek(value, SeekOrigin.Begin); + buffer = buffer[..(int)_remainingBytes]; } - /// - public override void Flush() => throw new NotSupportedException(); - /// - public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); - /// - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + if (buffer.Length <= 0) { - if ((count = BytesToRead(buffer, offset, count)) <= 0) - { - return 0; - } - int bytesRead = await _underlyingStream.ReadAsync(buffer, offset, count).ConfigureAwait(false); - _remainingBytes -= bytesRead; - return bytesRead; + return 0; } - /// - public override int Read(byte[] buffer, int offset, int count) + int bytesRead = await _underlyingStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _remainingBytes -= bytesRead; + return bytesRead; + } +#endif + /// + public override int Read(byte[] buffer, int offset, int count) + { + if ((count = BytesToRead(buffer, offset, count)) <= 0) + { + return 0; + } + int bytesRead = _underlyingStream.Read(buffer, offset, count); + _remainingBytes -= bytesRead; + return bytesRead; + } + private int BytesToRead(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0 || count < 0) { - if ((count = BytesToRead(buffer, offset, count)) <= 0) - { - return 0; - } - int bytesRead = _underlyingStream.Read(buffer, offset, count); - _remainingBytes -= bytesRead; - return bytesRead; + throw new ArgumentOutOfRangeException(); } - private int BytesToRead(byte[] buffer, int offset, int count) + if (offset + count > buffer.Length) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || count < 0) - { - throw new ArgumentOutOfRangeException(); - } - if (offset + count > buffer.Length) - { - throw new ArgumentException(); - } - return (int)Math.Min(count, _remainingBytes); + throw new ArgumentException(); } - /// - public override long Seek(long offset, SeekOrigin origin) + return (int)Math.Min(count, _remainingBytes); + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (!CanSeek) { - if (!CanSeek) - { - throw new NotSupportedException(); - } - // Recalculate offset relative to the current position - long newOffset = origin switch - { - SeekOrigin.Current => offset, - SeekOrigin.End => _length + offset - Position, - SeekOrigin.Begin => offset - Position, - _ => throw new ArgumentOutOfRangeException(nameof(origin)), - }; - // Determine whether the requested position is within the bounds of the stream - if (Position + newOffset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - long currentPosition = _underlyingStream.Position; - long newPosition = _underlyingStream.Seek(newOffset, SeekOrigin.Current); - _remainingBytes -= newPosition - currentPosition; - return Position; + throw new NotSupportedException(); } - /// - public override void SetLength(long value) => throw new NotSupportedException(); - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(); - /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - /// - protected override void Dispose(bool disposing) + // Recalculate offset relative to the current position + long newOffset = origin switch + { + SeekOrigin.Current => offset, + SeekOrigin.End => _length + offset - Position, + SeekOrigin.Begin => offset - Position, + _ => throw new ArgumentOutOfRangeException(nameof(origin)), + }; + // Determine whether the requested position is within the bounds of the stream + if (Position + newOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + long currentPosition = _underlyingStream.Position; + long newPosition = _underlyingStream.Seek(newOffset, SeekOrigin.Current); + _remainingBytes -= newPosition - currentPosition; + return Position; + } + /// + public override void SetLength(long value) => throw new NotSupportedException(); + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(); + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + /// + protected override void Dispose(bool disposing) + { + if (_remainingBytes != 0) { - if (_remainingBytes != 0) - { - _underlyingStream?.Dispose(); - } + _underlyingStream?.Dispose(); _underlyingStream = null; - Disposed?.Invoke(this, EventArgs.Empty); - base.Dispose(disposing); } + Disposed?.Invoke(this, EventArgs.Empty); + base.Dispose(disposing); } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/Listener.cs b/src/UiPath.CoreIpc/Server/Listener.cs index 0a3b0762..a9c10adf 100644 --- a/src/UiPath.CoreIpc/Server/Listener.cs +++ b/src/UiPath.CoreIpc/Server/Listener.cs @@ -1,80 +1,80 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Security.Cryptography.X509Certificates; -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; + +public class ListenerSettings +{ + public ListenerSettings(string name) => Name = name; + public byte ConcurrentAccepts { get; set; } = 5; + public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; + public X509Certificate Certificate { get; set; } + public string Name { get; } + public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; + internal IServiceProvider ServiceProvider { get; set; } + internal IDictionary Endpoints { get; set; } +} +abstract class Listener : IDisposable { - public class ListenerSettings + protected Listener(ListenerSettings settings) { - private bool _encryptAndSign; - - public ListenerSettings(string name) => Name = name; - public byte ConcurrentAccepts { get; set; } = 5; - public byte MaxReceivedMessageSizeInMegabytes { get; set; } = 2; - public bool EncryptAndSign - { - get => _encryptAndSign; -#if WINDOWS - set => _encryptAndSign = value; -#else - set => _encryptAndSign = false; -#endif - } - public string Name { get; } - public TimeSpan RequestTimeout { get; set; } = Timeout.InfiniteTimeSpan; - internal IServiceProvider ServiceProvider { get; set; } - internal IDictionary Endpoints { get; set; } + Settings = settings; + MaxMessageSize = settings.MaxReceivedMessageSizeInMegabytes * 1024 * 1024; } - abstract class Listener : IDisposable + 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) { - protected Listener(ListenerSettings settings) + Logger = ServiceProvider.GetRequiredService().CreateLogger(GetType()); + if (LogEnabled) { - Settings = settings; - MaxMessageSize = settings.MaxReceivedMessageSizeInMegabytes * 1024 * 1024; + Log($"Starting listener {Name}..."); } - 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) + return Task.WhenAll(Enumerable.Range(1, Settings.ConcurrentAccepts).Select(async _ => { - Logger = ServiceProvider.GetRequiredService().CreateLogger(GetType()); - return Task.WhenAll(Enumerable.Range(1, Settings.ConcurrentAccepts).Select(async _ => + while (!token.IsCancellationRequested) { - while (!token.IsCancellationRequested) - { - await AcceptConnection(token); - } - })); + await AcceptConnection(token); + } + })); + } + protected abstract ServerConnection CreateServerConnection(); + async Task AcceptConnection(CancellationToken token) + { + var serverConnection = CreateServerConnection(); + try + { + var network = await serverConnection.AcceptClient(token); + serverConnection.Listen(network, token).LogException(Logger, Name); } - protected abstract ServerConnection CreateServerConnection(); - async Task AcceptConnection(CancellationToken token) + catch (Exception ex) { - var serverConnection = CreateServerConnection(); - try + serverConnection.Dispose(); + if (!token.IsCancellationRequested) { - await serverConnection.AcceptClient(token); - serverConnection.Listen(token).LogException(Logger, Name); - } - catch (Exception ex) - { - serverConnection.Dispose(); - if (!token.IsCancellationRequested) - { - Logger.LogException(ex, Settings.Name); - } + Logger.LogException(ex, Settings.Name); } } - protected virtual void Dispose(bool disposing) { } - public void Dispose() + } + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + Settings.Certificate?.Dispose(); + } + public void Dispose() + { + if (LogEnabled) { - Dispose(disposing: true); - GC.SuppressFinalize(this); + Log($"Stopping listener {Name}..."); } + Dispose(disposing: true); + GC.SuppressFinalize(this); } + public void Log(string message) => Logger.LogInformation(message); + public bool LogEnabled => Logger.Enabled(); } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/Server.cs b/src/UiPath.CoreIpc/Server/Server.cs index 2364adb8..5e6d5b4d 100644 --- a/src/UiPath.CoreIpc/Server/Server.cs +++ b/src/UiPath.CoreIpc/Server/Server.cs @@ -1,239 +1,275 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Linq.Expressions; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace UiPath.CoreIpc +using System.Linq.Expressions; +namespace UiPath.CoreIpc; +using GetTaskResultFunc = Func; +using MethodExecutor = Func; +using static Expression; +using static CancellationTokenSourcePool; +class Server { - using GetTaskResultFunc = Func; - using MethodExecutor = Func; - using static Expression; - class Server + private static readonly MethodInfo GetResultMethod = typeof(Server).GetStaticMethod(nameof(GetTaskResultImpl)); + private static readonly ConcurrentDictionary<(Type,string), Method> Methods = new(); + private static readonly ConcurrentDictionary GetTaskResultByType = new(); + private readonly Connection _connection; + private readonly IClient _client; + private readonly ConcurrentDictionary _requests = new(); + public Server(ListenerSettings settings, Connection connection, IClient client = null) { - private static readonly MethodInfo GetResultMethod = typeof(Server).GetStaticMethod(nameof(GetTaskResultImpl)); - private static readonly ConcurrentDictionaryWrapper<(Type,string), Method> Methods = new(CreateMethod); - private static readonly ConcurrentDictionaryWrapper GetTaskResultByType = new(GetTaskResultFunc); - private readonly Connection _connection; - private readonly IClient _client; - private readonly CancellationTokenSource _connectionClosed = new(); - private readonly ConcurrentDictionary _requests = new(); - - public Server(ListenerSettings settings, Connection connection, IClient client = null, CancellationToken cancellationToken = default) + Settings = settings; + _connection = connection; + _client = client; + connection.RequestReceived += OnRequestReceived; + connection.CancellationReceived += CancelRequest; + connection.Closed += delegate { - Settings = settings; - _connection = connection; - _client = client; - connection.RequestReceived += OnRequestReceived; - connection.CancellationRequestReceived += requestId => + if (LogEnabled) { - if (_requests.TryGetValue(requestId, out var cancellation)) - { - cancellation.Cancel(); - } - }; - connection.Closed += delegate - { - Logger.LogDebug($"{Name} closed."); - _connectionClosed.Cancel(); - }; - return; - async Task OnRequestReceived(Request request, Stream uploadStream) + Log($"Server {Name} closed."); + } + foreach (var requestId in _requests.Keys) { try { - Logger.LogInformation($"{Name} received request {request}"); - if (!Endpoints.TryGetValue(request.Endpoint, out var endpoint)) - { - await OnError(new ArgumentOutOfRangeException(nameof(request.Endpoint), $"{Name} cannot find endpoint {request.Endpoint}")); - return; - } - Response response; - var requestCancellation = new CancellationTokenSource(); - _requests[request.Id] = requestCancellation; - var timeout = request.GetTimeout(Settings.RequestTimeout); - await timeout.Timeout(new() { cancellationToken, requestCancellation.Token, _connectionClosed.Token }, async token => - { - response = await HandleRequest(endpoint, token); - Logger.LogInformation($"{Name} sending response for {request}"); - await SendResponse(response, token); - return true; - }, request.MethodName, OnError); + CancelRequest(requestId); } catch (Exception ex) { - Logger.LogException(ex, $"{Name} {request}"); + Logger.LogException(ex, $"{Name}"); } - if (_requests.TryRemove(request.Id, out var cancellation)) + } + }; + } + void CancelRequest(string requestId) + { + if (_requests.TryRemove(requestId, out var cancellation)) + { + cancellation.Cancel(); + cancellation.Return(); + } + } +#if !NET461 + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + 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; + } + var method = GetMethod(endpoint.Contract, request.MethodName); + if (request.HasObjectParameters && !method.ReturnType.IsGenericType) + { + await HandleRequest(method, endpoint, request, default); + return; + } + Response response = null; + var requestCancellation = Rent(); + _requests[request.Id] = requestCancellation; + var timeout = request.GetTimeout(Settings.RequestTimeout); + var timeoutHelper = new TimeoutHelper(timeout, requestCancellation.Token); + try + { + var token = timeoutHelper.Token; + response = await HandleRequest(method, endpoint, request, token); + if (LogEnabled) { - cancellation.Dispose(); + Log($"{Name} sending response for {request}"); } - if (_connectionClosed.IsCancellationRequested) + await SendResponse(response, token); + } + catch (Exception ex) when(response == null) + { + await OnError(request, timeoutHelper.CheckTimeout(ex, request.MethodName)); + } + finally + { + timeoutHelper.Dispose(); + } + } + catch (Exception ex) + { + Logger.LogException(ex, $"{Name} {request}"); + } + if (_requests.TryRemove(request.Id, out var cancellation)) + { + cancellation.Return(); + } + } + ValueTask OnError(Request request, Exception ex) + { + Logger.LogException(ex, $"{Name} {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) + { + var objectParameters = request.HasObjectParameters; + var contract = endpoint.Contract; + var arguments = GetArguments(); + var beforeCall = endpoint.BeforeCall; + if (beforeCall != null) + { + 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 defaultScheduler = scheduler == TaskScheduler.Default; + if (returnTaskType.IsGenericType) + { + var methodResult = defaultScheduler ? MethodCall() : await RunOnScheduler(); + await methodResult; + var returnValue = GetTaskResult(returnTaskType, methodResult); + if (returnValue is Stream downloadStream) { - _connectionClosed.Dispose(); + return Response.Success(request, downloadStream); } - return; - async Task HandleRequest(EndpointSettings endpoint, CancellationToken cancellationToken) + return objectParameters ? new Response(request.Id, ObjectData: returnValue) : Response.Success(request, Serializer.Serialize(returnValue)); + } + else + { + (defaultScheduler ? MethodCall() : RunOnScheduler().Unwrap()).LogException(Logger, method.MethodInfo); + return objectParameters ? null : Response.Success(request, ""); + } + Task MethodCall() => method.Invoke(service, arguments, cancellationToken); + Task RunOnScheduler() => Task.Factory.StartNew(MethodCall, cancellationToken, TaskCreationOptions.DenyChildAttach, scheduler); + } + object[] GetArguments() + { + var parameters = method.Parameters; + var allParametersLength = parameters.Length; + var requestParametersLength = objectParameters ? request.ObjectParameters.Length : 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]; + Deserialize(); + SetOptionalArguments(); + return allArguments; + void Deserialize() + { + object argument; + for (int index = 0; index < requestParametersLength; index++) { - var contract = endpoint.Contract; - var method = GetMethod(contract, request.MethodName); - var arguments = GetArguments(); - var beforeCall = endpoint.BeforeCall; - if (beforeCall != null) + var parameterType = parameters[index].ParameterType; + if (parameterType == typeof(CancellationToken)) { - await beforeCall(new(default, request.MethodName, arguments), cancellationToken); + argument = null; } - IServiceScope scope = null; - var service = endpoint.ServiceInstance; - try + else if (parameterType == typeof(Stream)) { - if (service == null) - { - scope = ServiceProvider.CreateScope(); - service = scope.ServiceProvider.GetRequiredService(contract); - } - return await InvokeMethod(); + argument = request.UploadStream; } - finally + else { - scope?.Dispose(); - } - async Task InvokeMethod() - { - var returnTaskType = method.ReturnType; - var scheduler = endpoint.Scheduler; - if (returnTaskType.IsGenericType) - { - var methodResult = scheduler is null ? MethodCall() : await RunOnScheduler(); - await methodResult; - var returnValue = GetTaskResult(returnTaskType, methodResult); - return returnValue is Stream downloadStream ? Response.Success(request, downloadStream) : Response.Success(request, Serializer.Serialize(returnValue)); - } - else - { - (scheduler is null ? MethodCall() : RunOnScheduler()).LogException(Logger, method); - return Response.Success(request, ""); - } - Task MethodCall() - { - Logger.LogDebug($"Processing {method} on {Thread.CurrentThread.Name}."); - return method.Invoke(service, arguments); - } - Task RunOnScheduler() => Task.Factory.StartNew(MethodCall, cancellationToken, TaskCreationOptions.DenyChildAttach, scheduler); - } - object[] GetArguments() - { - var parameters = method.Parameters; - if (request.Parameters.Length > parameters.Length) - { - throw new ArgumentException("Too many parameters for " + method); - } - var allArguments = new object[parameters.Length]; - Deserialize(); - SetOptionalArguments(); - return allArguments; - void Deserialize() - { - object argument; - for (int index = 0; index < request.Parameters.Length; index++) - { - var parameterType = parameters[index].ParameterType; - if (parameterType == typeof(CancellationToken)) - { - argument = cancellationToken; - } - else if (parameterType == typeof(Stream)) - { - argument = uploadStream; - } - else - { - argument = Serializer.Deserialize(request.Parameters[index], parameterType); - argument = CheckMessage(argument, parameterType); - } - allArguments[index] = argument; - } - } - object CheckMessage(object argument, Type parameterType) - { - if (parameterType == typeof(Message) && argument == null) - { - argument = new Message(); - } - if (argument is Message message) - { - message.Endpoint = endpoint; - message.Client = _client; - } - return argument; - } - void SetOptionalArguments() - { - for (int index = request.Parameters.Length; index < parameters.Length; index++) - { - allArguments[index] = CheckMessage(method.Defaults[index], parameters[index].ParameterType); - } - } + argument = objectParameters ? + Serializer.Deserialize(request.ObjectParameters[index], parameterType) : + Serializer.Deserialize(request.Parameters[index], parameterType); + argument = CheckMessage(argument, parameterType); } + allArguments[index] = argument; } - Task OnError(Exception ex) + } + object CheckMessage(object argument, Type parameterType) + { + if (parameterType == typeof(Message) && argument == null) + { + argument = new Message(); + } + if (argument is Message message) { - Logger.LogException(ex, $"{Name} {request}"); - return SendResponse(Response.Fail(request, ex), cancellationToken); + message.CallbackContract = endpoint.CallbackContract; + message.Client = _client; + message.ObjectParameters = objectParameters; } + return argument; } - } - private ILogger Logger => _connection.Logger; - private ListenerSettings Settings { get; } - public IServiceProvider ServiceProvider => Settings.ServiceProvider; - public ISerializer Serializer => _connection.Serializer; - public string Name => _connection.Name; - public IDictionary Endpoints => Settings.Endpoints; - Task SendResponse(Response response, CancellationToken responseCancellation) => - _connectionClosed.IsCancellationRequested ? Task.CompletedTask : _connection.Send(response, responseCancellation); - static object GetTaskResultImpl(Task task) => ((Task)task).Result; - static object GetTaskResult(Type taskType, Task task) => - GetTaskResultByType.GetOrAdd(taskType.GenericTypeArguments[0])(task); - static GetTaskResultFunc GetTaskResultFunc(Type resultType) => GetResultMethod.MakeGenericDelegate(resultType); - static Method GetMethod(Type contract, string methodName) => Methods.GetOrAdd((contract, methodName)); - static Method CreateMethod((Type contract,string methodName) key) => new(key.contract.GetInterfaceMethod(key.methodName)); - readonly struct Method - { - static readonly ParameterExpression TargetParameter = Parameter(typeof(object), "target"); - static readonly ParameterExpression ParametersParameter = Parameter(typeof(object[]), "parameters"); - readonly MethodExecutor _executor; - readonly MethodInfo _methodInfo; - public readonly ParameterInfo[] Parameters; - public readonly object[] Defaults; - public Type ReturnType => _methodInfo.ReturnType; - public Method(MethodInfo method) - { - // https://github.com/dotnet/aspnetcore/blob/3f620310883092905ed6f13d784c908b5f4a9d7e/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs#L156 - var parameters = method.GetParameters(); - var callParameters = new Expression[parameters.Length]; - var defaults = new object[parameters.Length]; - for (int index = 0; index < parameters.Length; index++) + void SetOptionalArguments() + { + for (int index = requestParametersLength; index < allParametersLength; index++) { - var parameter = parameters[index]; - defaults[index] = parameter.GetDefaultValue(); - var paramValue = ArrayIndex(ParametersParameter, Constant(index, typeof(int))); - callParameters[index] = Convert(paramValue, parameter.ParameterType); + allArguments[index] = CheckMessage(method.Defaults[index], parameters[index].ParameterType); } - var instanceCast = Convert(TargetParameter, method.DeclaringType); - var methodCall = Call(instanceCast, method, callParameters); - var lambda = Lambda(methodCall, TargetParameter, ParametersParameter); - _executor = lambda.Compile(); - _methodInfo = method; - Parameters = parameters; - Defaults = defaults; - } - public Task Invoke(object service, object[] arguments) => _executor.Invoke(service, arguments); - public override string ToString() => _methodInfo.ToString(); + } + } + } + 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 + { + 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 readonly MethodInfo MethodInfo; + public readonly ParameterInfo[] Parameters; + public readonly object[] Defaults; + public Type ReturnType => MethodInfo.ReturnType; + public 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]; + for (int index = 0; index < parametersLength; index++) + { + var parameter = parameters[index]; + defaults[index] = parameter.GetDefaultValue(); + callParameters[index] = parameter.ParameterType == typeof(CancellationToken) ? TokenParameter : + Convert(ArrayIndex(ParametersParameter, Constant(index, typeof(int))), parameter.ParameterType); + } + var instanceCast = Convert(TargetParameter, method.DeclaringType); + var methodCall = Call(instanceCast, method, callParameters); + var lambda = Lambda(methodCall, TargetParameter, ParametersParameter, TokenParameter); + _executor = lambda.Compile(); + 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(); } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/ServerConnection.cs b/src/UiPath.CoreIpc/Server/ServerConnection.cs index 1bca6feb..43f11a6a 100644 --- a/src/UiPath.CoreIpc/Server/ServerConnection.cs +++ b/src/UiPath.CoreIpc/Server/ServerConnection.cs @@ -1,81 +1,85 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.IO; -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc +using System.Net.Security; +namespace UiPath.CoreIpc; + +public interface IClient { - public interface IClient - { - TCallbackInterface GetCallback(EndpointSettings endpoint) where TCallbackInterface : class; - void Impersonate(Action action); - } - abstract class ServerConnection : IClient, IDisposable + TCallbackInterface GetCallback(Type callbackContract, bool objectParameters) where TCallbackInterface : class; + void Impersonate(Action action); +} +abstract class ServerConnection : IClient, 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 { - private readonly ConcurrentDictionary _callbacks = new(); - protected readonly Listener _listener; - private Connection _connection; - 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); - protected abstract Stream Network { get; } - public virtual void Impersonate(Action action) => action(); - public TCallbackInterface GetCallback(EndpointSettings endpoint) where TCallbackInterface : class => - (TCallbackInterface)_callbacks.GetOrAdd(endpoint, CreateCallback); - TCallbackContract CreateCallback(EndpointSettings endpoint) where TCallbackContract : class + if (callbackContract == null) + { + throw new InvalidOperationException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's not configured."); + } + return (TCallbackInterface)_callbacks.GetOrAdd(callbackContract, CreateCallback); + TCallbackInterface CreateCallback(Type callbackContract) { - var configuredCallbackContract = endpoint.CallbackContract; - if (configuredCallbackContract == null || !typeof(TCallbackContract).IsAssignableFrom(configuredCallbackContract)) + if (!typeof(TCallbackInterface).IsAssignableFrom(callbackContract)) { - throw new ArgumentException($"Callback contract mismatch. Requested {typeof(TCallbackContract)}, but it's {configuredCallbackContract?.ToString() ?? "not configured"}."); + throw new ArgumentException($"Callback contract mismatch. Requested {typeof(TCallbackInterface)}, but it's {callbackContract}."); } - Logger.LogInformation($"Create callback {_listener.Name}"); - var serviceClient = new ServiceClient(_connection.Serializer, Settings.RequestTimeout, Logger, (_,_) => Task.FromResult(_connection)); + if (_listener.LogEnabled) + { + _listener.Log($"Create callback {callbackContract} {_listener.Name}"); + } + _connectionAsTask ??= Task.FromResult(_connection); + var serviceClient = new ServiceClient(_connection.Serializer, Settings.RequestTimeout, Logger, (_, _) => _connectionAsTask) + { + ObjectParameters = objectParameters + }; return serviceClient.CreateProxy(); } - public async Task Listen(CancellationToken cancellationToken) + } + 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); + // close the connection when the service host closes + using (cancellationToken.UnsafeRegister(state => ((Connection)state).Dispose(), _connection)) + { + await _connection.Listen(); + } + return; + async Task AuthenticateAsServer() { - var stream = await AuthenticateAsServer(); - var serializer = Settings.ServiceProvider.GetRequiredService(); - _connection = new(stream, serializer, Logger, _listener.Name, _listener.MaxMessageSize); - _server = new(Settings, _connection, this, cancellationToken); - // close the connection when the service host closes - using (cancellationToken.Register(_connection.Dispose)) + var certificate = Settings.Certificate; + if (certificate == null) { - await _connection.Listen(); + return network; } - return; - async Task AuthenticateAsServer() + var sslStream = new SslStream(network); + try { - if (!Settings.EncryptAndSign) - { - return Network; - } - var negotiateStream = new NegotiateStream(Network); - try - { - await negotiateStream.AuthenticateAsServerAsync(); - } - catch - { - negotiateStream.Dispose(); - throw; - } - Debug.Assert(negotiateStream.IsEncrypted && negotiateStream.IsSigned); - return negotiateStream; + await sslStream.AuthenticateAsServerAsync(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); - } + } + protected virtual void Dispose(bool disposing){} + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Server/ServiceHost.cs b/src/UiPath.CoreIpc/Server/ServiceHost.cs index 92e3fda5..13a834b1 100644 --- a/src/UiPath.CoreIpc/Server/ServiceHost.cs +++ b/src/UiPath.CoreIpc/Server/ServiceHost.cs @@ -1,61 +1,34 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace UiPath.CoreIpc +namespace UiPath.CoreIpc; +public sealed class ServiceHost : IDisposable { - public class ServiceHost : IDisposable + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly IDictionary _endpoints; + private readonly IReadOnlyCollection _listeners; + internal ServiceHost(IEnumerable listeners, IDictionary endpoints) { - private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly IDictionary _endpoints; - private readonly IReadOnlyCollection _listeners; - private readonly ILogger _logger; - - internal ServiceHost(IEnumerable listeners, IDictionary endpoints, IServiceProvider serviceProvider) - { - _endpoints = endpoints.ToReadOnlyDictionary(); - _listeners = listeners.ToArray(); - _logger = serviceProvider.GetRequiredService>(); - } - - public IServiceProvider ServiceProvider => _endpoints.Values.FirstOrDefault()?.ServiceProvider; - - public void Dispose() + _endpoints = endpoints.ToReadOnlyDictionary(); + _listeners = listeners.ToArray(); + } + public void Dispose() + { + if(_cancellationTokenSource.IsCancellationRequested) { - if(_cancellationTokenSource.IsCancellationRequested) - { - return; - } - foreach (var listener in _listeners) - { - listener.Dispose(); - } - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); + return; } - - public void Run() + foreach (var listener in _listeners) { - RunAsync().Wait(); + listener.Dispose(); } - - public Task RunAsync(TaskScheduler taskScheduler = null) + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.AssertDisposed(); + } + public void Run() => RunAsync().Wait(); + public Task RunAsync(TaskScheduler taskScheduler = null) + { + foreach (var endpoint in _endpoints.Values) { - foreach (var endpoint in _endpoints.Values) - { - endpoint.Scheduler = taskScheduler; - } - var tasks = _listeners.Select(listener => Task.Run(() => - { - _logger.LogDebug($"Starting endpoint '{listener}'..."); - _cancellationTokenSource.Token.Register(() => _logger.LogInformation($"Stopping endpoint '{listener}'...")); - return listener.Listen(_cancellationTokenSource.Token).ContinueWith(_ => _logger.LogInformation($"Endpoint '{listener}' stopped.")); - })); - return Task.WhenAll(tasks); + 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 index c890cfc2..e5c3474f 100644 --- a/src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs +++ b/src/UiPath.CoreIpc/Server/ServiceHostBuilder.cs @@ -1,80 +1,74 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +namespace UiPath.CoreIpc; -namespace UiPath.CoreIpc +using BeforeCallHandler = Func; +public class ServiceHostBuilder { - 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) { - 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, ServiceProvider); + settings.ServiceProvider = ServiceProvider; + Endpoints.Add(settings.Name, settings); + return this; } - public static class ServiceHostBuilderExtensions + internal ServiceHostBuilder AddListener(Listener listener) { - 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)); + listener.Settings.ServiceProvider = ServiceProvider; + listener.Settings.Endpoints = Endpoints; + _listeners.Add(listener); + return this; } - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddIpc(this IServiceCollection services) - { - services.AddSingleton(); - return services; - } - } - public class EndpointSettings + public ServiceHost Build() => new(_listeners, Endpoints); +} +public static class ServiceHostBuilderExtensions +{ + public static ServiceHostBuilder AddEndpoints(this ServiceHostBuilder serviceHostBuilder, IEnumerable endpoints) { - public EndpointSettings(Type contract, object serviceInstance = null, Type callbackContract = null) + foreach (var endpoint in endpoints) { - Contract = contract ?? throw new ArgumentNullException(nameof(contract)); - Name = contract.Name; - ServiceInstance = serviceInstance; - CallbackContract = callbackContract; + serviceHostBuilder.AddEndpoint(endpoint); } - internal string Name { get; } - internal TaskScheduler Scheduler { get; set; } - 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); + return serviceHostBuilder; } - public class EndpointSettings : EndpointSettings where TContract : class + 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) { - public EndpointSettings(TContract serviceInstance = null, Type callbackContract = null) : base(typeof(TContract), serviceInstance, callbackContract) { } + services.AddSingleton(); + return services; } - public class EndpointSettings : EndpointSettings where TContract : class where TCallbackContract : class +} +public class EndpointSettings +{ + private TaskScheduler _scheduler; + public EndpointSettings(Type contract, object serviceInstance = null, Type callbackContract = null) { - public EndpointSettings(TContract serviceInstance = null) : base(serviceInstance, typeof(TCallbackContract)) { } + 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/TaskCompletionPool.cs b/src/UiPath.CoreIpc/TaskCompletionPool.cs new file mode 100644 index 00000000..11f9ece9 --- /dev/null +++ b/src/UiPath.CoreIpc/TaskCompletionPool.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks.Sources; +namespace UiPath.CoreIpc; +internal static class TaskCompletionPool +{ + public static ManualResetValueTaskSource Rent() => ObjectPool.Rent(); + static void Return(ManualResetValueTaskSource source) => ObjectPool.Return(source); + public sealed class ManualResetValueTaskSource : IValueTaskSource, IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; // mutable struct; do not make this readonly + public bool RunContinuationsAsynchronously { get => _core.RunContinuationsAsynchronously; set => _core.RunContinuationsAsynchronously = value; } + public short Version => _core.Version; + public ValueTask ValueTask() => new(this, Version); + public void Reset() => _core.Reset(); + public void SetResult(T result) => _core.SetResult(result); + public void SetException(Exception error) => _core.SetException(error); + public void SetCanceled() => _core.SetException(new TaskCanceledException()); + 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 Return() + { + Reset(); + TaskCompletionPool.Return(this); + } + } +} \ No newline at end of file diff --git a/src/UiPath.CoreIpc/Tcp/TcpClient.cs b/src/UiPath.CoreIpc/Tcp/TcpClient.cs index de195824..50386575 100644 --- a/src/UiPath.CoreIpc/Tcp/TcpClient.cs +++ b/src/UiPath.CoreIpc/Tcp/TcpClient.cs @@ -1,46 +1,42 @@ -using Microsoft.Extensions.Logging; -using System; -using System.IO; -using System.Net; +using System.Net; using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.Tcp +namespace UiPath.CoreIpc.Tcp; + +using ConnectionFactory = Func>; +using BeforeCallHandler = Func; +interface ITcpKey : IConnectionKey +{ + IPEndPoint EndPoint { get; } +} +class TcpClient : ServiceClient, ITcpKey where TInterface : class { - using ConnectionFactory = Func>; - using BeforeCallHandler = Func; - interface ITcpKey : IConnectionKey + 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) { - IPEndPoint EndPoint { get; } + EndPoint = endPoint; + HashCode = (EndPoint, sslServer).GetHashCode(); } - class TcpClient : ServiceClient, ITcpKey where TInterface : class + 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 { - public TcpClient(IPEndPoint endPoint, ISerializer serializer, TimeSpan requestTimeout, ILogger logger, ConnectionFactory connectionFactory, bool encryptAndSign, BeforeCallHandler beforeCall, EndpointSettings serviceEndpoint) : base(serializer, requestTimeout, logger, connectionFactory, encryptAndSign, beforeCall, serviceEndpoint) => - EndPoint = endPoint; - public override string Name => base.Name ?? EndPoint.ToString(); - public IPEndPoint EndPoint { get; } - public override int GetHashCode() => EndPoint.GetHashCode(); - public override bool Equals(IConnectionKey other) => other == this || (other is ITcpKey otherClient && EndPoint.Equals(otherClient.EndPoint)); - public override ClientConnection CreateClientConnection(IConnectionKey key) => new TcpClientConnection(key); - 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) { - 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 Stream Network => _tcpClient.GetStream(); - public override async Task Connect(CancellationToken cancellationToken) - { - _tcpClient = new(); - using var token = cancellationToken.Register(Dispose); - var endPoint = ((ITcpKey)ConnectionKey).EndPoint; - await _tcpClient.ConnectAsync(endPoint.Address, endPoint.Port); - } + _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 index 3e964e0a..a0a05b71 100644 --- a/src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs +++ b/src/UiPath.CoreIpc/Tcp/TcpClientBuilder.cs @@ -1,39 +1,36 @@ -using System; -using System.Net; -using System.Threading.Tasks; +using System.Net; -namespace UiPath.CoreIpc.Tcp +namespace UiPath.CoreIpc.Tcp; + +public abstract class TcpClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder { - public abstract class TcpClientBuilderBase : ServiceClientBuilder where TInterface : class where TDerived : ServiceClientBuilder - { - private readonly IPEndPoint _endPoint; + private readonly IPEndPoint _endPoint; - protected TcpClientBuilderBase(IPEndPoint endPoint, Type callbackContract = null, IServiceProvider serviceProvider = null) : base(callbackContract, serviceProvider) => - _endPoint = 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, _encryptAndSign, _beforeCall, serviceEndpoint).CreateProxy(); - } + 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 class TcpClientBuilder : TcpClientBuilderBase, TInterface> where TInterface : class + public TcpClientBuilder CallbackInstance(TCallbackInterface singleton) { - public TcpClientBuilder(IPEndPoint endPoint) : base(endPoint){} + _callbackInstance = singleton; + return this; } - public class TcpClientBuilder : TcpClientBuilderBase, TInterface> where TInterface : class where TCallbackInterface : class + public TcpClientBuilder TaskScheduler(TaskScheduler taskScheduler) { - 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; - } + _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 index abf60a93..b2019e3c 100644 --- a/src/UiPath.CoreIpc/Tcp/TcpListener.cs +++ b/src/UiPath.CoreIpc/Tcp/TcpListener.cs @@ -1,55 +1,47 @@ -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -namespace UiPath.CoreIpc.Tcp +using System.Net; +namespace UiPath.CoreIpc.Tcp; + +public class TcpSettings : ListenerSettings { - public class TcpSettings : ListenerSettings + public TcpSettings(IPEndPoint endPoint) : base(endPoint.ToString()) { - public TcpSettings(IPEndPoint endPoint) : base(endPoint.ToString()) - { - EndPoint = endPoint; - } - public IPEndPoint EndPoint { get; } + 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(); } - class TcpListener : Listener + Task AcceptClient(CancellationToken cancellationToken) => _tcpServer.AcceptTcpClientAsync(); + class TcpServerConnection : ServerConnection { - readonly System.Net.Sockets.TcpListener _tcpServer; - public TcpListener(ListenerSettings settings) : base(settings) + System.Net.Sockets.TcpClient _tcpClient; + public TcpServerConnection(Listener listener) : base(listener){} + public override async Task AcceptClient(CancellationToken cancellationToken) { - _tcpServer = new(Settings.EndPoint); - _tcpServer.Start(backlog: Settings.ConcurrentAccepts); + _tcpClient = await ((TcpListener)_listener).AcceptClient(cancellationToken); + return _tcpClient.GetStream(); } - public new TcpSettings Settings => (TcpSettings)base.Settings; - protected override ServerConnection CreateServerConnection() => new TcpServerConnection(this); protected override void Dispose(bool disposing) { + _tcpClient?.Dispose(); base.Dispose(disposing); - _tcpServer.Stop(); - } - async Task AcceptClient(CancellationToken cancellationToken) - { - using (cancellationToken.Register(Dispose)) - { - return await _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); - protected override void Dispose(bool disposing) - { - _tcpClient?.Dispose(); - base.Dispose(disposing); - } - protected override Stream Network => _tcpClient.GetStream(); } } - public static class TcpServiceExtensions - { - public static ServiceHostBuilder UseTcp(this ServiceHostBuilder builder, TcpSettings settings) => builder.AddListener(new TcpListener(settings)); - } +} +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/UiPath.CoreIpc.csproj b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj index 31007d94..b0111764 100644 --- a/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj +++ b/src/UiPath.CoreIpc/UiPath.CoreIpc.csproj @@ -1,29 +1,44 @@  - - net461;netstandard2.0;net5.0-windows + net6.0;net461;net6.0-windows UiPath.CoreIpc true UiPath - 2.1.0 + 2.5.1 https://github.com/UiPath/CoreIpc/ + README.md json-rpc rpc ipc netcore wcf true - WCF-like service model API for communication over named pipes. + WCF-like service model API for communication over named pipes, TCP and web sockets. MIT true snupkg + CA1416 + latest + true - + + + + - + + <_Parameter1>UiPath.CoreIpc.Tests + + + - - + + - - - + + + + + + + + + - - + \ No newline at end of file diff --git a/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs b/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs new file mode 100644 index 00000000..f0774988 --- /dev/null +++ b/src/UiPath.CoreIpc/WebSockets/WebSocketClient.cs @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..893a8b7a --- /dev/null +++ b/src/UiPath.CoreIpc/WebSockets/WebSocketClientBuilder.cs @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..20b95a06 --- /dev/null +++ b/src/UiPath.CoreIpc/WebSockets/WebSocketListener.cs @@ -0,0 +1,24 @@ +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/WebSockets/WebSocketStream.cs b/src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs new file mode 100644 index 00000000..c64b4816 --- /dev/null +++ b/src/UiPath.CoreIpc/WebSockets/WebSocketStream.cs @@ -0,0 +1,69 @@ +using System.Net.WebSockets; +namespace UiPath.CoreIpc.WebSockets; +/// +/// Exposes a as a . +/// https://github.com/AArnott/Nerdbank.Streams/blob/main/src/Nerdbank.Streams/WebSocketStream.cs +/// +public class WebSocketStream : Stream +{ + /// + /// The socket wrapped by this stream. + /// + private readonly WebSocket _webSocket; + /// + /// Initializes a new instance of the class. + /// + /// The web socket to wrap in a stream. + public WebSocketStream(WebSocket webSocket) => _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); + /// + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_webSocket.CloseStatus.HasValue) + { + return 0; + } + var result = await _webSocket.ReceiveAsync(new(buffer, offset, count), cancellationToken).ConfigureAwait(false); + return result.Count; + } + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _webSocket.SendAsync(new(buffer, offset, count), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken); + /// + public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + /// + public override void Write(byte[] buffer, int offset, int count) => WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); + /// + /// Does nothing, since web sockets do not need to be flushed. + /// + public override void Flush(){} + /// + /// Does nothing, since web sockets do not need to be flushed. + /// + /// An ignored cancellation token. + /// A completed task. + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// + protected override void Dispose(bool disposing) + { + _webSocket.Dispose(); + base.Dispose(disposing); + } + /// + public override bool CanRead => true; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => true; + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + /// + public override void SetLength(long value) => throw new NotSupportedException(); + /// + public override long Length => throw new NotSupportedException(); + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } +} \ No newline at end of file