diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 93df4e04f9a3..a9b8019ab76c 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -38,6 +38,19 @@ variables: value: '' - name: _SignType value: '' + - name: _InternalRuntimeDownloadArgs + value: '' + - name: _InternalRuntimeDownloadCodeSignArgs + value: '' +- ${{ if eq(variables['System.TeamProject'], 'internal') }}: + - group: DotNet-MSRC-Storage + - name: _InternalRuntimeDownloadArgs + value: -DotNetRuntimeSourceFeed https://dotnetclimsrc.blob.core.windows.net/dotnet -DotNetRuntimeSourceFeedKey $(dotnetclimsrc-read-sas-token-base64) /p:DotNetAssetRootAccessTokenSuffix='$(dotnetclimsrc-read-sas-token-base64)' + # The code signing doesn't use the aspnet build scripts, so the msbuild parameers have + # to be passed directly. This is awkward, since we pass the same info above, but we have + # to have it in two different forms + - name: _InternalRuntimeDownloadCodeSignArgs + value: /p:DotNetRuntimeSourceFeed=https://dotnetclimsrc.blob.core.windows.net/dotnet /p:DotNetRuntimeSourceFeedKey=$(dotnetclimsrc-read-sas-token-base64) - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: # DotNet-Blob-Feed provides: dotnetfeed-storage-access-key-1 @@ -81,7 +94,15 @@ stages: jobDisplayName: Code check agentOs: Windows steps: - - powershell: ./eng/scripts/CodeCheck.ps1 -ci + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - powershell: ./eng/scripts/CodeCheck.ps1 -ci $(_InternalRuntimeDownloadArgs) displayName: Run eng/scripts/CodeCheck.ps1 artifacts: - name: Code_Check_Logs @@ -108,6 +129,14 @@ stages: # This is intentional to workaround https://github.com/dotnet/arcade/issues/1957 which always re-submits for code-signing, even # if they have already been signed. This results in slower builds due to re-submitting the same .nupkg many times for signing. # The sign settings have been configured to + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) - script: ./build.cmd -ci @@ -117,6 +146,7 @@ stages: -buildNative /bl:artifacts/log/build.x64.binlog $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) displayName: Build x64 # Build the x86 shared framework @@ -132,6 +162,7 @@ stages: /p:OnlyPackPlatformSpecificPackages=true /bl:artifacts/log/build.x86.binlog $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) displayName: Build x86 # This is in a separate build step with -forceCoreMsbuild to workaround MAX_PATH limitations - https://github.com/Microsoft/msbuild/issues/53 @@ -140,6 +171,7 @@ stages: -pack -noBuildDeps $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) displayName: Build SiteExtension # This runs code-signing on all packages, zips, and jar files as defined in build/CodeSign.targets. If https://github.com/dotnet/arcade/issues/1957 is resolved, @@ -165,6 +197,7 @@ stages: /p:AssetManifestFileName=aspnetcore-win-x64-x86.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) /p:PublishInstallerBaseVersion=true displayName: Build Installers @@ -205,6 +238,7 @@ stages: /p:AssetManifestFileName=aspnetcore-win-arm.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -231,6 +265,7 @@ stages: -p:AssetManifestFileName=aspnetcore-MacOS_x64.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -251,6 +286,14 @@ stages: jobDisplayName: "Build: Linux x64" agentOs: Linux steps: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: Bash@3 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + arguments: $(Build.SourcesDirectory)/NuGet.config $Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) - script: ./build.sh --ci --arch x64 @@ -261,6 +304,7 @@ stages: -p:OnlyPackPlatformSpecificPackages=true -bl:artifacts/log/build.linux-x64.binlog $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) displayName: Run build.sh - script: | git clean -xfd src/**/obj/ @@ -274,7 +318,8 @@ stages: -p:BuildRuntimeArchive=false \ -p:LinuxInstallerType=deb \ -bl:artifacts/log/build.deb.binlog \ - $(_BuildArgs) + $(_BuildArgs) \ + $(_InternalRuntimeDownloadArgs) displayName: Build Debian installers - script: | git clean -xfd src/**/obj/ @@ -290,7 +335,8 @@ stages: -bl:artifacts/log/build.rpm.binlog \ -p:AssetManifestFileName=aspnetcore-Linux_x64.xml \ $(_BuildArgs) \ - $(_PublishArgs) + $(_PublishArgs) \ + $(_InternalRuntimeDownloadArgs) displayName: Build RPM installers installNodeJs: false installJdk: false @@ -322,6 +368,7 @@ stages: -p:AssetManifestFileName=aspnetcore-Linux_arm.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -352,6 +399,7 @@ stages: -p:AssetManifestFileName=aspnetcore-Linux_arm64.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -385,6 +433,7 @@ stages: -p:AssetManifestFileName=aspnetcore-Linux_musl_x64.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -418,6 +467,7 @@ stages: -p:AssetManifestFileName=aspnetcore-Linux_musl_arm64.xml $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadArgs) installNodeJs: false installJdk: false artifacts: @@ -439,7 +489,7 @@ stages: jobDisplayName: "Test: Windows Server 2016 x64" agentOs: Windows isTestingJob: true - buildArgs: -all -pack -test -BuildNative "/p:SkipIISNewHandlerTests=true /p:SkipIISTests=true /p:SkipIISExpressTests=true /p:SkipIISNewShimTests=true /p:RunTemplateTests=false" + buildArgs: -all -pack -test -BuildNative "/p:SkipIISNewHandlerTests=true /p:SkipIISTests=true /p:SkipIISExpressTests=true /p:SkipIISNewShimTests=true /p:RunTemplateTests=false" $(_InternalRuntimeDownloadArgs) beforeBuild: - powershell: "& ./src/Servers/IIS/tools/UpdateIISExpressCertificate.ps1; & ./src/Servers/IIS/tools/update_schema.ps1" displayName: Setup IISExpress test certificates and schema @@ -475,7 +525,15 @@ stages: agentOs: Windows isTestingJob: true steps: - - script: ./build.cmd -ci -all -pack + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - script: ./build.cmd -ci -all -pack $(_InternalRuntimeDownloadArgs) displayName: Build Repo - script: ./src/ProjectTemplates/build.cmd -ci -pack -NoRestore -NoBuilddeps "/p:RunTemplateTests=true /bl:artifacts/log/template.pack.binlog" displayName: Pack Templates @@ -502,7 +560,7 @@ stages: jobDisplayName: "Test: macOS 10.13" agentOs: macOS isTestingJob: true - buildArgs: --all --test "/p:RunTemplateTests=false" + buildArgs: --all --test "/p:RunTemplateTests=false" $(_InternalRuntimeDownloadArgs) beforeBuild: - bash: "./eng/scripts/install-nginx-mac.sh" displayName: Installing Nginx @@ -537,7 +595,7 @@ stages: jobDisplayName: "Test: Ubuntu 16.04 x64" agentOs: Linux isTestingJob: true - buildArgs: --all --test "/p:RunTemplateTests=false" + buildArgs: --all --test "/p:RunTemplateTests=false" $(_InternalRuntimeDownloadArgs) beforeBuild: - bash: "./eng/scripts/install-nginx-linux.sh" displayName: Installing Nginx @@ -584,6 +642,25 @@ stages: chmod +x $HOME/bin/jq echo "##vso[task.prependpath]$HOME/bin" displayName: Install jq + - task: UseDotNet@2 + displayName: 'Use .NET Core sdk' + inputs: + packageType: sdk + # The SDK version selected here is intentionally supposed to use the latest release + # For the purpose of building Linux distros, we can't depend on features of the SDK + # which may not exist in pre-built versions of the SDK + # Pinning to preview 8 since preview 9 has breaking changes + version: 3.1.100 + installationPath: $(DotNetCoreSdkDir) + includePreviewVersions: true + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: Bash@3 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + arguments: $(Build.SourcesDirectory)/NuGet.config $Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) - script: ./eng/scripts/ci-source-build.sh --ci --configuration Release /p:BuildManaged=true /p:BuildNodeJs=false displayName: Run ci-source-build.sh - task: PublishBuildArtifacts@1 diff --git a/.azure/pipelines/jobs/codesign-xplat.yml b/.azure/pipelines/jobs/codesign-xplat.yml index abf15125404a..07e2e99f0e89 100644 --- a/.azure/pipelines/jobs/codesign-xplat.yml +++ b/.azure/pipelines/jobs/codesign-xplat.yml @@ -28,6 +28,14 @@ jobs: contents: '**/*.nupkg' targetFolder: $(Build.SourcesDirectory)/artifacts/packages/$(BuildConfiguration)/shipping/ flattenFolders: true + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) - powershell: .\eng\common\build.ps1 -ci -restore @@ -39,6 +47,7 @@ jobs: /p:DotNetSignType=$(_SignType) $(_BuildArgs) $(_PublishArgs) + $(_InternalRuntimeDownloadCodeSignArgs) displayName: Sign and publish packages artifacts: - name: CodeSign_Xplat_${{ parameters.inputName }}_Logs diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index d218c44531c2..cba72a653c94 100644 --- a/.azure/pipelines/jobs/default-build.yml +++ b/.azure/pipelines/jobs/default-build.yml @@ -161,6 +161,24 @@ jobs: - ${{ if ne(parameters.steps, '')}}: - ${{ parameters.steps }} - ${{ if eq(parameters.steps, '')}}: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - ${{ if eq(parameters.agentOs, 'Windows') }}: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - ${{ if ne(parameters.agentOs, 'Windows') }}: + - task: Bash@3 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + arguments: $(Build.SourcesDirectory)/NuGet.config $Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) - ${{ if eq(parameters.buildScript, '') }}: - ${{ if eq(parameters.agentOs, 'Windows') }}: - script: .\$(BuildDirectory)\build.cmd -ci /p:DotNetSignType=$(_SignType) -Configuration $(BuildConfiguration) $(BuildScriptArgs) diff --git a/Directory.Build.props b/Directory.Build.props index b123605a339f..aa477ec9ed07 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -84,8 +84,8 @@ aspnetcore-runtime aspnetcore-targeting-pack - - + + false true diff --git a/build.ps1 b/build.ps1 index c515a84af5b3..cf854f54b4c3 100644 --- a/build.ps1 +++ b/build.ps1 @@ -77,6 +77,12 @@ MSBuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] .PARAMETER MSBuildArguments Additional MSBuild arguments to be passed through. +.PARAMETER DotNetRuntimeSourceFeed +Additional feed that can be used when downloading .NET runtimes + +.PARAMETER DotNetRuntimeSourceFeedKey +Key for feed that can be used when downloading .NET runtimes + .EXAMPLE Building both native and managed projects. @@ -151,6 +157,11 @@ param( # Other lifecycle targets [switch]$Help, # Show help + + # Optional arguments that enable downloading an internal + # runtime or runtime from a non-default location + [string]$DotNetRuntimeSourceFeed, + [string]$DotNetRuntimeSourceFeedKey, # Capture the rest [Parameter(ValueFromRemainingArguments = $true)] @@ -251,6 +262,16 @@ if (-not $Configuration) { } $MSBuildArguments += "/p:Configuration=$Configuration" +[string[]]$ToolsetBuildArguments = @() +if ($DotNetRuntimeSourceFeed -or $DotNetRuntimeSourceFeedKey) { + $runtimeFeedArg = "/p:DotNetRuntimeSourceFeed=$DotNetRuntimeSourceFeed" + $runtimeFeedKeyArg = "/p:DotNetRuntimeSourceFeedKey=$DotNetRuntimeSourceFeedKey" + $MSBuildArguments += $runtimeFeedArg + $MSBuildArguments += $runtimeFeedKeyArg + $ToolsetBuildArguments += $runtimeFeedArg + $ToolsetBuildArguments += $runtimeFeedKeyArg +} + $foundJdk = $false $javac = Get-Command javac -ErrorAction Ignore -CommandType Application $localJdkPath = "$PSScriptRoot\.tools\jdk\win-x64\" @@ -375,7 +396,8 @@ try { /p:Configuration=Release ` /p:Restore=$RunRestore ` /p:Build=true ` - /clp:NoSummary + /clp:NoSummary ` + @ToolsetBuildArguments } MSBuild $toolsetBuildProj ` diff --git a/build.sh b/build.sh index 05e162836823..b97417c4fae0 100755 --- a/build.sh +++ b/build.sh @@ -29,6 +29,8 @@ build_installers='' build_projects='' target_arch='x64' configuration='' +dotnet_runtime_source_feed='' +dotnet_runtime_source_feed_key='' if [ "$(uname)" = "Darwin" ]; then target_os_name='osx' @@ -45,33 +47,36 @@ __usage() { echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] [[--] ...] Arguments: - ... Arguments passed to the command. Variable number of arguments allowed. + ... Arguments passed to the command. Variable number of arguments allowed. Options: - --configuration|-c The build configuration (Debug, Release). Default=Debug - --arch The CPU architecture to build for (x64, arm, arm64). Default=$target_arch - --os-name The base runtime identifier to build for (linux, osx, linux-musl). Default=$target_os_name - - --[no-]restore Run restore. - --[no-]build Compile projects. (Implies --no-restore) - --[no-]pack Produce packages. - --[no-]test Run tests. - - --projects A list of projects to build. (Must be an absolute path.) - Globbing patterns are supported, such as \"$(pwd)/**/*.csproj\". - --no-build-deps Do not build project-to-project references and only build the specified project. - --no-build-repo-tasks Suppress building RepoTasks. - - --all Build all project types. - --[no-]build-native Build native projects (C, C++). - --[no-]build-managed Build managed projects (C#, F#, VB). - --[no-]build-nodejs Build NodeJS projects (TypeScript, JS). - --[no-]build-java Build Java projects. - --[no-]build-installers Build Java projects. - - --ci Apply CI specific settings and environment variables. - --binarylog|-bl Use a binary logger - --verbosity|-v MSBuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] + --configuration|-c The build configuration (Debug, Release). Default=Debug + --arch The CPU architecture to build for (x64, arm, arm64). Default=$target_arch + --os-name The base runtime identifier to build for (linux, osx, linux-musl). Default=$target_os_name + + --[no-]restore Run restore. + --[no-]build Compile projects. (Implies --no-restore) + --[no-]pack Produce packages. + --[no-]test Run tests. + + --projects A list of projects to build. (Must be an absolute path.) + Globbing patterns are supported, such as \"$(pwd)/**/*.csproj\". + --no-build-deps Do not build project-to-project references and only build the specified project. + --no-build-repo-tasks Suppress building RepoTasks. + + --all Build all project types. + --[no-]build-native Build native projects (C, C++). + --[no-]build-managed Build managed projects (C#, F#, VB). + --[no-]build-nodejs Build NodeJS projects (TypeScript, JS). + --[no-]build-java Build Java projects. + --[no-]build-installers Build Java projects. + + --ci Apply CI specific settings and environment variables. + --binarylog|-bl Use a binary logger + --verbosity|-v MSBuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] + + --dotnet-runtime-source-feed Additional feed that can be used when downloading .NET runtimes + --dotnet-runtime-source-feed-key Key for feed that can be used when downloading .NET runtimes Description: This build script installs required tools and runs an MSBuild command on this repository @@ -188,16 +193,26 @@ while [[ $# -gt 0 ]]; do -no-build-repo-tasks|-nobuildrepotasks) build_repo_tasks=false ;; + -arch) + shift + target_arch="${1:-}" + [ -z "$target_arch" ] && __error "Missing value for parameter --arch" && __usage + ;; -ci) ci=true ;; -binarylog|-bl) use_default_binary_log=true ;; - -verbosity|-v) + -dotnet-runtime-source-feed|-dotnetruntimesourcefeed) shift - [ -z "${1:-}" ] && __error "Missing value for parameter --verbosity" && __usage - verbosity="${1:-}" + [ -z "${1:-}" ] && __error "Missing value for parameter --dotnet-runtime-source-feed" && __usage + dotnet_runtime_source_feed="${1:-}" + ;; + -dotnet-runtime-source-feed-key|-dotnetruntimesourcefeedkey) + shift + [ -z "${1:-}" ] && __error "Missing value for parameter --dotnet-runtime-source-feed-key" && __usage + dotnet_runtime_source_feed_key="${1:-}" ;; *) msbuild_args[${#msbuild_args[*]}]="$1" @@ -270,6 +285,17 @@ msbuild_args[${#msbuild_args[*]}]="-p:Configuration=$configuration" echo "Setting msbuild verbosity to $verbosity" msbuild_args[${#msbuild_args[*]}]="-verbosity:$verbosity" +# Set up additional runtime args +toolset_build_args=() +if [ ! -z "$dotnet_runtime_source_feed" ] || [ ! -z "$dotnet_runtime_source_feed_key" ]; then + runtimeFeedArg="/p:DotNetRuntimeSourceFeed=$dotnet_runtime_source_feed" + runtimeFeedKeyArg="/p:DotNetRuntimeSourceFeedKey=$dotnet_runtime_source_feed_key" + msbuild_args[${#msbuild_args[*]}]=$runtimeFeedArg + msbuild_args[${#msbuild_args[*]}]=$runtimeFeedKeyArg + toolset_build_args[${#toolset_build_args[*]}]=$runtimeFeedArg + toolset_build_args[${#toolset_build_args[*]}]=$runtimeFeedKeyArg +fi + # Initialize global variables need to be set before the import of Arcade is imported restore=$run_restore @@ -325,7 +351,8 @@ if [ "$build_repo_tasks" = true ]; then -p:Configuration=Release \ -p:Restore=$run_restore \ -p:Build=true \ - -clp:NoSummary + -clp:NoSummary \ + ${toolset_build_args[@]+"${toolset_build_args[@]}"} fi # This incantation avoids unbound variable issues if msbuild_args is empty diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index b5c81965354d..05508591091c 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,106 +2,106 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.1.0 + 3.1.1 - 3.0.0 + 3.0.2 - 3.0.0 + 3.0.2 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - - - + + + - + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 @@ -109,39 +109,39 @@ - 3.1.0 + 3.1.1 - - - + + + - - - + + + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + @@ -186,273 +186,273 @@ - 3.1.0 + 3.1.1 - - - + + + - - - + + + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - - + + - - + + - 3.1.0 + 3.1.1 - + - + - 3.1.0 + 3.1.1 - - - - + + + + - - - - + + + + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - + - + - + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - + - + - 3.1.0 + 3.1.1 - - - - - - + + + + + + - - - - - - + + + + + + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - - + + - - + + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - - - + + + - - - + + + - 3.1.0 + 3.1.1 - + - + - 3.1.0 + 3.1.1 - + - + - 3.1.0 + 3.1.1 - - + + - - + + - 3.1.0 + 3.1.1 - - - - + + + + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 @@ -460,236 +460,238 @@ - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - - - + + + - 3.1.0 + 3.1.1 - - - + + + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - + - - + + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - - + + - - + + - - - - + + + + - 3.1.0 + 3.1.1 - - + + - - + + - 3.1.0 + 3.1.1 - + - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - - - - + + + + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - + - 3.1.0 + 3.1.1 - - + + - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - 3.1.0 + 3.1.1 - - - + + + - 3.1.0 + 3.1.1 - - - + + + - - - + + + - 3.1.0 + 3.1.1 - - + + + - - + + + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index bdcbf3e91ddf..d479dfe6ce0a 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,86 +4,86 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0fffa6bcd5fb..e8fabf2c39f0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -13,281 +13,281 @@ https://github.com/aspnet/Blazor 7868699de745fd30a654c798a99dc541b77b95c0 - - https://github.com/aspnet/AspNetCore-Tooling - 9cf15bed711983d35362d62972ab87ccc88ba8ca + + https://dev.azure.com/dnceng/internal/_git/aspnet-AspNetCore-Tooling + 07f16c89db55ab6f9f773cc3db6eb5a52908065e - - https://github.com/aspnet/AspNetCore-Tooling - 9cf15bed711983d35362d62972ab87ccc88ba8ca + + https://dev.azure.com/dnceng/internal/_git/aspnet-AspNetCore-Tooling + 07f16c89db55ab6f9f773cc3db6eb5a52908065e - - https://github.com/aspnet/AspNetCore-Tooling - 9cf15bed711983d35362d62972ab87ccc88ba8ca + + https://dev.azure.com/dnceng/internal/_git/aspnet-AspNetCore-Tooling + 07f16c89db55ab6f9f773cc3db6eb5a52908065e - - https://github.com/aspnet/AspNetCore-Tooling - 9cf15bed711983d35362d62972ab87ccc88ba8ca + + https://dev.azure.com/dnceng/internal/_git/aspnet-AspNetCore-Tooling + 07f16c89db55ab6f9f773cc3db6eb5a52908065e - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/EntityFrameworkCore - 7f59c0bb92fb33698232f0d7007ebcb0a428b543 + + https://dev.azure.com/dnceng/internal/_git/aspnet-EntityFrameworkCore + fde8a73d63ea11e1f2bdc690cf1b16ba0a449649 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 https://github.com/dotnet/corefx @@ -377,25 +377,25 @@ https://github.com/dotnet/corefx 0f7f38c4fd323b26da10cce95f857f77f0f09b48 - - https://github.com/dotnet/core-setup - f3f2dd583fffa254015fc21ff0e45784b333256d + + https://dev.azure.com/dnceng/internal/_git/dotnet-core-setup + a1388f194c30cb21b36b75982962cb5e35954e4e - - https://github.com/dotnet/core-setup - f3f2dd583fffa254015fc21ff0e45784b333256d + + https://dev.azure.com/dnceng/internal/_git/dotnet-core-setup + a1388f194c30cb21b36b75982962cb5e35954e4e https://github.com/dotnet/core-setup 7d57652f33493fa022125b7f63aad0d70c52d810 - - https://github.com/dotnet/core-setup - f3f2dd583fffa254015fc21ff0e45784b333256d + + https://dev.azure.com/dnceng/internal/_git/dotnet-core-setup + a1388f194c30cb21b36b75982962cb5e35954e4e @@ -413,9 +413,9 @@ https://github.com/dotnet/corefx 0f7f38c4fd323b26da10cce95f857f77f0f09b48 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 https://github.com/dotnet/arcade @@ -429,9 +429,9 @@ https://github.com/dotnet/arcade 4d80b9cfa53e309c8f685abff3512f60c3d8a3d1 - - https://github.com/aspnet/Extensions - d40e21ccc14908a054b2181b1d6aeb22c49c630d + + https://dev.azure.com/dnceng/internal/_git/aspnet-Extensions + d00c382ec5d68a85d2eb4a49ab4559b8db7a2390 https://github.com/dotnet/roslyn diff --git a/eng/Versions.props b/eng/Versions.props index ac32a23cfdbc..adeb6806fb14 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,12 +8,12 @@ 3 1 - 1 + 2 0 - false + true release true false @@ -66,10 +66,10 @@ 3.4.0-beta4-19569-03 - 3.1.1-servicing.19576.9 - 3.1.1-servicing.19576.9 + 3.1.1 + 3.1.1-servicing.19608.4 3.1.0 - 3.1.1-servicing.19576.9 + 3.1.1 2.1.0 1.1.0 @@ -99,80 +99,80 @@ 3.1.0-preview4.19605.1 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 - 3.1.1-servicing.19604.6 + 3.1.1-servicing.19614.4 + 3.1.1-servicing.19614.4 + 3.1.1-servicing.19614.4 + 3.1.1-servicing.19614.4 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1 + 3.1.1-servicing.19614.4 + 3.1.1-servicing.19614.4 + 3.1.1 3.1.0-rtm.19565.4 - 3.1.1-servicing.19604.6 - 3.1.1-preview4.19604.6 + 3.1.1 + 3.1.1-preview4.19614.4 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 - 3.1.1-servicing.19605.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 - 3.1.1-servicing.19605.6 - 3.1.1-servicing.19605.6 - 3.1.1-servicing.19605.6 - 3.1.1-servicing.19605.6 + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 https://dotnetcli.blob.core.windows.net/dotnet/ + https://dotnetclimsrc.blob.core.windows.net/dotnet/ diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 94965a8fd2a9..62218302268f 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -210,7 +210,15 @@ function InstallDotNet { local runtimeSourceFeedKey='' if [[ -n "${7:-}" ]]; then - decodedFeedKey=`echo $7 | base64 --decode` + # The 'base64' binary on alpine uses '-d' and doesn't support '--decode' + # like the rest of the unix variants. At the same time, MacOS doesn't support + # '-d'. To work around this, do a simple detection and switch the parameter + # accordingly. + decodeArg="--decode" + if base64 --help 2>&1 | grep -q "BusyBox"; then + decodeArg="-d" + fi + decodedFeedKey=`echo $7 | base64 $decodeArg` runtimeSourceFeedKey="--feed-credential $decodedFeedKey" fi diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1 index 0d9c9dfec8a5..072f55fe2196 100644 --- a/eng/scripts/CodeCheck.ps1 +++ b/eng/scripts/CodeCheck.ps1 @@ -4,7 +4,11 @@ This script runs a quick check for common errors, such as checking that Visual Studio solutions are up to date or that generated code has been committed to source. #> param( - [switch]$ci + [switch]$ci, + # Optional arguments that enable downloading an internal + # runtime or runtime from a non-default location + [string]$DotNetRuntimeSourceFeed, + [string]$DotNetRuntimeSourceFeedKey ) $ErrorActionPreference = 'Stop' @@ -43,7 +47,12 @@ function LogError { try { if ($ci) { # Install dotnet.exe - & $repoRoot/restore.cmd -ci -NoBuildNodeJS + if ($DotNetRuntimeSourceFeed -or $DotNetRuntimeSourceFeedKey) { + & $repoRoot/restore.cmd -ci -NoBuildNodeJS -DotNetRuntimeSourceFeed $DotNetRuntimeSourceFeed -DotNetRuntimeSourceFeedKey $DotNetRuntimeSourceFeedKey + } + else{ + & $repoRoot/restore.cmd -ci -NoBuildNodeJS + } } . "$repoRoot/activate.ps1" @@ -171,12 +180,13 @@ try { # Redirect stderr to stdout because PowerShell does not consistently handle output to stderr $changedFiles = & cmd /c 'git --no-pager diff --ignore-space-at-eol --name-only 2>nul' - # Temporary: Disable check for blazor js file - $changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js" + # Temporary: Disable check for blazor js file and nuget.config (updated automatically for + # internal builds) + $changedFilesExclusions = @("src/Components/Web.JS/dist/Release/blazor.server.js", "NuGet.config") if ($changedFiles) { foreach ($file in $changedFiles) { - if ($file -eq $changedFilesExclusion) {continue} + if ($changedFilesExclusions -contains $file) {continue} $filePath = Resolve-Path "${repoRoot}/${file}" LogError "Generated code is not up to date in $file. You might need to regenerate the reference assemblies or project list (see docs/ReferenceAssemblies.md and docs/ReferenceResolution.md)" -filepath $filePath diff --git a/eng/scripts/ci-source-build.sh b/eng/scripts/ci-source-build.sh index 3387125d7823..ebc50dad0a59 100755 --- a/eng/scripts/ci-source-build.sh +++ b/eng/scripts/ci-source-build.sh @@ -9,10 +9,31 @@ set -euo pipefail scriptroot="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" reporoot="$(dirname "$(dirname "$scriptroot")")" + # For local development, make a backup copy of this file first +if [ ! -f "$reporoot/global.bak.json" ]; then + mv "$reporoot/global.json" "$reporoot/global.bak.json" +fi + + # Detect the current version of .NET Core installed +export SDK_VERSION=$(dotnet --version) +echo "The ambient version of .NET Core SDK version = $SDK_VERSION" + + # Update the global.json file to match the current .NET environment +cat "$reporoot/global.bak.json" | \ + jq '.sdk.version=env.SDK_VERSION' | \ + jq '.tools.dotnet=env.SDK_VERSION' | \ + jq 'del(.tools.runtimes)' \ + > "$reporoot/global.json" + + # Restore the original global.json file +trap "{ + mv "$reporoot/global.bak.json" "$reporoot/global.json" +}" EXIT + # Build repo tasks "$reporoot/eng/common/build.sh" --restore --build --ci --configuration Release /p:ProjectToBuild=$reporoot/eng/tools/RepoTasks/RepoTasks.csproj export DotNetBuildFromSource='true' # Build projects -"$reporoot/eng/common/build.sh" --restore --build --pack "$@" +"$reporoot/eng/common/build.sh" --restore --build --pack "$@" \ No newline at end of file diff --git a/eng/tools/RepoTasks/DownloadFile.cs b/eng/tools/RepoTasks/DownloadFile.cs new file mode 100644 index 000000000000..2be0954cc258 --- /dev/null +++ b/eng/tools/RepoTasks/DownloadFile.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation and contributors. 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace RepoTasks +{ + public class DownloadFile : Microsoft.Build.Utilities.Task + { + [Required] + public string Uri { get; set; } + + /// + /// If this field is set and the task fail to download the file from `Uri`, with a NotFound + /// status, it will try to download the file from `PrivateUri`. + /// + public string PrivateUri { get; set; } + + /// + /// Suffix for the private URI in base64 form (for SAS compatibility) + /// + public string PrivateUriSuffix { get; set; } + + public int MaxRetries { get; set; } = 5; + + [Required] + public string DestinationPath { get; set; } + + public bool Overwrite { get; set; } + + public override bool Execute() + { + return ExecuteAsync().GetAwaiter().GetResult(); + } + + private async System.Threading.Tasks.Task ExecuteAsync() + { + string destinationDir = Path.GetDirectoryName(DestinationPath); + if (!Directory.Exists(destinationDir)) + { + Directory.CreateDirectory(destinationDir); + } + + if (File.Exists(DestinationPath) && !Overwrite) + { + return true; + } + + const string FileUriProtocol = "file://"; + + if (Uri.StartsWith(FileUriProtocol, StringComparison.Ordinal)) + { + var filePath = Uri.Substring(FileUriProtocol.Length); + Log.LogMessage($"Copying '{filePath}' to '{DestinationPath}'"); + File.Copy(filePath, DestinationPath); + return true; + } + + List errorMessages = new List(); + bool? downloadStatus = await DownloadWithRetriesAsync(Uri, DestinationPath, errorMessages); + + if (downloadStatus == false && !string.IsNullOrEmpty(PrivateUri)) + { + string uriSuffix = ""; + if (!string.IsNullOrEmpty(PrivateUriSuffix)) + { + var uriSuffixBytes = System.Convert.FromBase64String(PrivateUriSuffix); + uriSuffix = System.Text.Encoding.UTF8.GetString(uriSuffixBytes); + } + downloadStatus = await DownloadWithRetriesAsync($"{PrivateUri}{uriSuffix}", DestinationPath, errorMessages); + } + + if (downloadStatus != true) + { + foreach (var error in errorMessages) + { + Log.LogError(error); + } + } + + return downloadStatus == true; + } + + /// + /// Attempt to download file from `source` with retries when response error is different of FileNotFound and Success. + /// + /// URL to the file to be downloaded. + /// Local path where to put the downloaded file. + /// true: Download Succeeded. false: Download failed with 404. null: Download failed but is retriable. + private async Task DownloadWithRetriesAsync(string source, string target, List errorMessages) + { + Random rng = new Random(); + + Log.LogMessage(MessageImportance.High, $"Attempting download '{source}' to '{target}'"); + + using (var httpClient = new HttpClient()) + { + for (int retryNumber = 0; retryNumber < MaxRetries; retryNumber++) + { + try + { + var httpResponse = await httpClient.GetAsync(source); + + Log.LogMessage(MessageImportance.High, $"{source} -> {httpResponse.StatusCode}"); + + // The Azure Storage REST API returns '400 - Bad Request' in some cases + // where the resource is not found on the storage. + // https://docs.microsoft.com/en-us/rest/api/storageservices/common-rest-api-error-codes + if (httpResponse.StatusCode == HttpStatusCode.NotFound || + httpResponse.ReasonPhrase.IndexOf("The requested URI does not represent any resource on the server.", StringComparison.OrdinalIgnoreCase) == 0) + { + errorMessages.Add($"Problems downloading file from '{source}'. Does the resource exist on the storage? {httpResponse.StatusCode} : {httpResponse.ReasonPhrase}"); + return false; + } + + httpResponse.EnsureSuccessStatusCode(); + + using (var outStream = File.Create(target)) + { + await httpResponse.Content.CopyToAsync(outStream); + } + + Log.LogMessage(MessageImportance.High, $"returning true {source} -> {httpResponse.StatusCode}"); + return true; + } + catch (Exception e) + { + Log.LogMessage(MessageImportance.High, $"returning error in {source} "); + errorMessages.Add($"Problems downloading file from '{source}'. {e.Message} {e.StackTrace}"); + File.Delete(target); + } + + await System.Threading.Tasks.Task.Delay(rng.Next(1000, 10000)); + } + } + + Log.LogMessage(MessageImportance.High, $"giving up {source} "); + errorMessages.Add($"Giving up downloading the file from '{source}' after {MaxRetries} retries."); + return null; + } + } +} diff --git a/eng/tools/RepoTasks/RepoTasks.tasks b/eng/tools/RepoTasks/RepoTasks.tasks index 5fcc97d1567f..4916a97ed395 100644 --- a/eng/tools/RepoTasks/RepoTasks.tasks +++ b/eng/tools/RepoTasks/RepoTasks.tasks @@ -10,4 +10,5 @@ + diff --git a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj index 62d93eef5a79..4a7b1c9c4f89 100644 --- a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj +++ b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj @@ -38,6 +38,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant dotnet-runtime-$(MicrosoftNETCoreAppRuntimeVersion)-$(TargetRuntimeIdentifier)$(ArchiveExtension) $(DotNetAssetRootUrl)Runtime/$(MicrosoftNETCoreAppInternalPackageVersion)/$(DotNetRuntimeArchiveFileName) + $(DotNetPrivateAssetRootUrl)Runtime/$(MicrosoftNETCoreAppInternalPackageVersion)/$(DotNetRuntimeArchiveFileName) $(BaseIntermediateOutputPath)$(DotNetRuntimeArchiveFileName) @@ -379,9 +380,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant --> + Uri="$(DotNetRuntimeDownloadUrl)" + PrivateUri="$(DotNetRuntimePrivateDownloadUrl)" + PrivateUriSuffix="$(DotNetAssetRootAccessTokenSuffix)" + DestinationPath="$(DotNetRuntimeArchive)" /> https://dotnetcli.azureedge.net/dotnet/ $(DotNetAssetRootUrl)/ + https://dotnetclimsrc.azureedge.net/dotnet/ + $(DotNetPrivateAssetRootUrl)/ - + dotnet-runtime-$(MicrosoftNETCoreAppRuntimeVersion)-win-x64.exe - + dotnet-runtime-$(MicrosoftNETCoreAppRuntimeVersion)-win-x86.exe @@ -37,7 +39,10 @@ + Uri="$(DotNetAssetRootUrl)%(RemoteAsset.Identity)" + PrivateUri="$(DotNetPrivateAssetRootUrl)%(RemoteAsset.Identity)" + PrivateUriSuffix="$(DotNetAssetRootAccessTokenSuffix)" + DestinationPath="$(DepsPath)%(RemoteAsset.TargetFileName)" /> diff --git a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets index 7cc09195fe81..948108395aaa 100644 --- a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets +++ b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets @@ -15,7 +15,7 @@ %(_TargetingPackVersionInfo.PackageVersion) - $(AspNetCoreBaselineVersion) + $(TargetingPackVersionPrefix) diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs index dac620efa8f5..abf6b695246a 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs @@ -31,6 +31,8 @@ internal class HttpConnectionContext : ConnectionContext, IHttpTransportFeature, IConnectionInherentKeepAliveFeature { + private static long _tenSeconds = TimeSpan.FromSeconds(10).Ticks; + private readonly object _stateLock = new object(); private readonly object _itemsLock = new object(); private readonly object _heartbeatLock = new object(); @@ -40,6 +42,12 @@ internal class HttpConnectionContext : ConnectionContext, private IDuplexPipe _application; private IDictionary _items; + private CancellationTokenSource _sendCts; + private bool _activeSend; + private long _startedSendTime; + private readonly object _sendingLock = new object(); + internal CancellationToken SendingToken { get; private set; } + // This tcs exists so that multiple calls to DisposeAsync all wait asynchronously // on the same task private readonly TaskCompletionSource _disposeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -258,8 +266,26 @@ private async Task WaitOnTasks(Task applicationTask, Task transportTask, bool cl } else { - // The other transports don't close their own output, so we can do it here safely - Application?.Output.Complete(); + // Normally it isn't safe to try and acquire this lock because the Send can hold onto it for a long time if there is backpressure + // It is safe to wait for this lock now because the Send will be in one of 4 states + // 1. In the middle of a write which is in the middle of being canceled by the CancelPendingFlush above, when it throws + // an OperationCanceledException it will complete the PipeWriter which will make any other Send waiting on the lock + // throw an InvalidOperationException if they call Write + // 2. About to write and see that there is a pending cancel from the CancelPendingFlush, go to 1 to see what happens + // 3. Enters the Send and sees the Dispose state from DisposeAndRemoveAsync and releases the lock + // 4. No Send in progress + await WriteLock.WaitAsync(); + try + { + // Complete the applications read loop + Application?.Output.Complete(); + } + finally + { + WriteLock.Release(); + } + + Application?.Input.CancelPendingRead(); } } @@ -401,7 +427,7 @@ public bool TryActivateLongPollingConnection( nonClonedContext.Response.RegisterForDispose(timeoutSource); nonClonedContext.Response.RegisterForDispose(tokenSource); - var longPolling = new LongPollingServerTransport(timeoutSource.Token, Application.Input, loggerFactory); + var longPolling = new LongPollingServerTransport(timeoutSource.Token, Application.Input, loggerFactory, this); // Start the transport TransportTask = longPolling.ProcessRequestAsync(nonClonedContext, tokenSource.Token); @@ -507,6 +533,40 @@ private async Task ExecuteApplication(ConnectionDelegate connectionDelegate) await connectionDelegate(this); } + internal void StartSendCancellation() + { + lock (_sendingLock) + { + if (_sendCts == null || _sendCts.IsCancellationRequested) + { + _sendCts = new CancellationTokenSource(); + SendingToken = _sendCts.Token; + } + _startedSendTime = DateTime.UtcNow.Ticks; + _activeSend = true; + } + } + internal void TryCancelSend(long currentTicks) + { + lock (_sendingLock) + { + if (_activeSend) + { + if (currentTicks - _startedSendTime > _tenSeconds) + { + _sendCts.Cancel(); + } + } + } + } + internal void StopSendCancellation() + { + lock (_sendingLock) + { + _activeSend = false; + } + } + private static class Log { private static readonly Action _disposingConnection = diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs index 9da1ea0c185d..c40a80b9ce97 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs @@ -142,7 +142,7 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti connection.SupportedFormats = TransferFormat.Text; // We only need to provide the Input channel since writing to the application is handled through /send. - var sse = new ServerSentEventsServerTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory); + var sse = new ServerSentEventsServerTransport(connection.Application.Input, connection.ConnectionId, connection, _loggerFactory); await DoPersistentConnection(connectionDelegate, sse, context, connection); } @@ -216,7 +216,9 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti connection.Transport.Output.Complete(connection.ApplicationTask.Exception); // Wait for the transport to run - await connection.TransportTask; + // Ignore exceptions, it has been logged if there is one and the application has finished + // So there is no one to give the exception to + await connection.TransportTask.NoThrow(); // If the status code is a 204 it means the connection is done if (context.Response.StatusCode == StatusCodes.Status204NoContent) @@ -234,12 +236,12 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti connection.MarkInactive(); } } - else if (resultTask.IsFaulted) + else if (resultTask.IsFaulted || resultTask.IsCanceled) { // Cancel current request to release any waiting poll and let dispose acquire the lock currentRequestTcs.TrySetCanceled(); - - // transport task was faulted, we should remove the connection + // We should be able to safely dispose because there's no more data being written + // We don't need to wait for close here since we've already waited for both sides await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false); } else @@ -434,6 +436,14 @@ private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOpti context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain"; + + // There are no writes anymore (since this is the write "loop") + // So it is safe to complete the writer + // We complete the writer here because we already have the WriteLock acquired + // and it's unsafe to complete outside of the lock + // Other code isn't guaranteed to be able to acquire the lock before another write + // even if CancelPendingFlush is called, and the other write could hang if there is backpressure + connection.Application.Output.Complete(); return; } catch (IOException ex) @@ -481,11 +491,8 @@ private async Task ProcessDeleteAsync(HttpContext context) Log.TerminatingConection(_logger); - // Complete the receiving end of the pipe - connection.Application.Output.Complete(); - - // Dispose the connection gracefully, but don't wait for it. We assign it here so we can wait in tests - connection.DisposeAndRemoveTask = _manager.DisposeAndRemoveAsync(connection, closeGracefully: true); + // Dispose the connection, but don't wait for it. We assign it here so we can wait in tests + connection.DisposeAndRemoveTask = _manager.DisposeAndRemoveAsync(connection, closeGracefully: false); context.Response.StatusCode = StatusCodes.Status202Accepted; context.Response.ContentType = "text/plain"; diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs index 4a97681fc0b6..b0f4b079fb34 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs @@ -31,6 +31,7 @@ internal partial class HttpConnectionManager private readonly TimerAwaitable _nextHeartbeat; private readonly ILogger _logger; private readonly ILogger _connectionLogger; + private readonly bool _useSendTimeout = true; private readonly TimeSpan _disconnectTimeout; public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime) @@ -44,6 +45,11 @@ public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifet _connectionLogger = loggerFactory.CreateLogger(); _nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate); _disconnectTimeout = connectionOptions.Value.DisconnectTimeout ?? ConnectionOptionsSetup.DefaultDisconectTimeout; + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Http.Connections.DoNotUseSendTimeout", out var timeoutDisabled)) + { + _useSendTimeout = !timeoutDisabled; + } + // Register these last as the callbacks could run immediately appLifetime.ApplicationStarted.Register(() => Start()); appLifetime.ApplicationStopping.Register(() => CloseConnections()); @@ -155,20 +161,26 @@ public void Scan() // Capture the connection state var lastSeenUtc = connection.LastSeenUtcIfInactive; + var utcNow = DateTimeOffset.UtcNow; // Once the decision has been made to dispose we don't check the status again // But don't clean up connections while the debugger is attached. - if (!Debugger.IsAttached && lastSeenUtc.HasValue && (DateTimeOffset.UtcNow - lastSeenUtc.Value).TotalSeconds > _disconnectTimeout.TotalSeconds) + if (!Debugger.IsAttached && lastSeenUtc.HasValue && (utcNow - lastSeenUtc.Value).TotalSeconds > _disconnectTimeout.TotalSeconds) { Log.ConnectionTimedOut(_logger, connection.ConnectionId); HttpConnectionsEventSource.Log.ConnectionTimedOut(connection.ConnectionId); // This is most likely a long polling connection. The transport here ends because - // a poll completed and has been inactive for > 5 seconds so we wait for the + // a poll completed and has been inactive for > 5 seconds so we wait for the // application to finish gracefully _ = DisposeAndRemoveAsync(connection, closeGracefully: true); } else { + if (!Debugger.IsAttached && _useSendTimeout) + { + connection.TryCancelSend(utcNow.Ticks); + } + // Tick the heartbeat, if the connection is still active connection.TickHeartbeat(); } diff --git a/src/SignalR/common/Http.Connections/src/Internal/TaskExtensions.cs b/src/SignalR/common/Http.Connections/src/Internal/TaskExtensions.cs new file mode 100644 index 000000000000..9608a6727264 --- /dev/null +++ b/src/SignalR/common/Http.Connections/src/Internal/TaskExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Runtime.CompilerServices; +namespace System.Threading.Tasks +{ + internal static class TaskExtensions + { + public static async Task NoThrow(this Task task) + { + await new NoThrowAwaiter(task); + } + } + internal readonly struct NoThrowAwaiter : ICriticalNotifyCompletion + { + private readonly Task _task; + public NoThrowAwaiter(Task task) { _task = task; } + public NoThrowAwaiter GetAwaiter() => this; + public bool IsCompleted => _task.IsCompleted; + // Observe exception + public void GetResult() { _ = _task.Exception; } + public void OnCompleted(Action continuation) => _task.GetAwaiter().OnCompleted(continuation); + public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation); + } +} \ No newline at end of file diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs index 02ff32ab8fb8..3432e3703914 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs @@ -16,12 +16,19 @@ internal class LongPollingServerTransport : IHttpTransport private readonly PipeReader _application; private readonly ILogger _logger; private readonly CancellationToken _timeoutToken; + private readonly HttpConnectionContext _connection; public LongPollingServerTransport(CancellationToken timeoutToken, PipeReader application, ILoggerFactory loggerFactory) + : this(timeoutToken, application, loggerFactory, connection: null) + { } + + public LongPollingServerTransport(CancellationToken timeoutToken, PipeReader application, ILoggerFactory loggerFactory, HttpConnectionContext connection) { _timeoutToken = timeoutToken; _application = application; + _connection = connection; + // We create the logger with a string to preserve the logging namespace after the server side transport renames. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Connections.Internal.Transports.LongPollingTransport"); } @@ -33,7 +40,7 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok var result = await _application.ReadAsync(token); var buffer = result.Buffer; - if (buffer.IsEmpty && result.IsCompleted) + if (buffer.IsEmpty && (result.IsCompleted || result.IsCanceled)) { Log.LongPolling204(_logger); context.Response.ContentType = "text/plain"; @@ -51,19 +58,22 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok try { - await context.Response.Body.WriteAsync(buffer); + _connection?.StartSendCancellation(); + await context.Response.Body.WriteAsync(buffer, _connection?.SendingToken ?? default); } finally { + _connection?.StopSendCancellation(); _application.AdvanceTo(buffer.End); } } catch (OperationCanceledException) { - // 3 cases: + // 4 cases: // 1 - Request aborted, the client disconnected (no response) // 2 - The poll timeout is hit (200) - // 3 - A new request comes in and cancels this request (204) + // 3 - SendingToken was canceled, abort the connection + // 4 - A new request comes in and cancels this request (204) // Case 1 if (context.RequestAborted.IsCancellationRequested) @@ -81,9 +91,16 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status200OK; } - else + else if (_connection?.SendingToken.IsCancellationRequested == true) { // Case 3 + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = StatusCodes.Status204NoContent; + throw; + } + else + { + // Case 4 Log.LongPolling204(_logger); context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status204NoContent; diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs index 54f2ed8f38bb..3d5e1f6f4bd5 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs @@ -16,11 +16,17 @@ internal class ServerSentEventsServerTransport : IHttpTransport private readonly PipeReader _application; private readonly string _connectionId; private readonly ILogger _logger; + private readonly HttpConnectionContext _connection; public ServerSentEventsServerTransport(PipeReader application, string connectionId, ILoggerFactory loggerFactory) + : this(application, connectionId, connection: null, loggerFactory) + { } + + public ServerSentEventsServerTransport(PipeReader application, string connectionId, HttpConnectionContext connection, ILoggerFactory loggerFactory) { _application = application; _connectionId = connectionId; + _connection = connection; // We create the logger with a string to preserve the logging namespace after the server side transport renames. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Connections.Internal.Transports.ServerSentEventsTransport"); @@ -51,11 +57,17 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok try { + if (result.IsCanceled) + { + break; + } + if (!buffer.IsEmpty) { Log.SSEWritingMessage(_logger, buffer.Length); - await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, context.Response.Body); + _connection?.StartSendCancellation(); + await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, context.Response.Body, _connection?.SendingToken ?? default); } else if (result.IsCompleted) { @@ -64,6 +76,7 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok } finally { + _connection?.StopSendCancellation(); _application.AdvanceTo(buffer.End); } } diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs index d5c2c1fefb2e..a95041c48a81 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs @@ -231,7 +231,8 @@ private async Task StartSending(WebSocket socket) if (WebSocketCanSend(socket)) { - await socket.SendAsync(buffer, webSocketMessageType); + _connection.StartSendCancellation(); + await socket.SendAsync(buffer, webSocketMessageType, _connection.SendingToken); } else { @@ -254,6 +255,7 @@ private async Task StartSending(WebSocket socket) } finally { + _connection.StopSendCancellation(); _application.Input.AdvanceTo(buffer.End); } } diff --git a/src/SignalR/common/Http.Connections/src/ServerSentEventsMessageFormatter.cs b/src/SignalR/common/Http.Connections/src/ServerSentEventsMessageFormatter.cs index efd2e24f0f90..6e723c51681b 100644 --- a/src/SignalR/common/Http.Connections/src/ServerSentEventsMessageFormatter.cs +++ b/src/SignalR/common/Http.Connections/src/ServerSentEventsMessageFormatter.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http.Connections @@ -15,19 +16,19 @@ internal static class ServerSentEventsMessageFormatter private const byte LineFeed = (byte)'\n'; - public static async Task WriteMessageAsync(ReadOnlySequence payload, Stream output) + public static async Task WriteMessageAsync(ReadOnlySequence payload, Stream output, CancellationToken token) { // Payload does not contain a line feed so write it directly to output if (payload.PositionOf(LineFeed) == null) { if (payload.Length > 0) { - await output.WriteAsync(DataPrefix, 0, DataPrefix.Length); - await output.WriteAsync(payload); - await output.WriteAsync(Newline, 0, Newline.Length); + await output.WriteAsync(DataPrefix, 0, DataPrefix.Length, token); + await output.WriteAsync(payload, token); + await output.WriteAsync(Newline, 0, Newline.Length, token); } - await output.WriteAsync(Newline, 0, Newline.Length); + await output.WriteAsync(Newline, 0, Newline.Length, token); return; } @@ -37,7 +38,7 @@ public static async Task WriteMessageAsync(ReadOnlySequence payload, Strea await WriteMessageToMemory(ms, payload); ms.Position = 0; - await ms.CopyToAsync(output); + await ms.CopyToAsync(output, token); } /// diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs index d543cd4f9abf..2dabe1adbc52 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs @@ -1050,6 +1050,178 @@ public async Task LongPollingTimeoutSets200StatusCode() } } + private class BlockingStream : Stream + { + private readonly SyncPoint _sync; + private bool _isSSE; + public BlockingStream(SyncPoint sync, bool isSSE = false) + { + _sync = sync; + _isSSE = isSSE; + } + public override bool CanRead => throw new NotImplementedException(); + public override bool CanSeek => throw new NotImplementedException(); + public override bool CanWrite => throw new NotImplementedException(); + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + public override void Flush() + { + } + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_isSSE) + { + // SSE does an initial write of :\r\n that we want to ignore in testing + _isSSE = false; + return; + } + await _sync.WaitToContinue(); + cancellationToken.ThrowIfCancellationRequested(); + } +#if NETCOREAPP2_1 + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (_isSSE) + { + // SSE does an initial write of :\r\n that we want to ignore in testing + _isSSE = false; + return; + } + await _sync.WaitToContinue(); + cancellationToken.ThrowIfCancellationRequested(); + } +#endif + } + + [Fact] + [LogLevel(LogLevel.Debug)] + public async Task LongPollingConnectionClosesWhenSendTimeoutReached() + { + bool ExpectedErrors(WriteContext writeContext) + { + return (writeContext.LoggerName == typeof(Internal.Transports.LongPollingServerTransport).FullName && + writeContext.EventId.Name == "LongPollingTerminated") || + (writeContext.LoggerName == typeof(HttpConnectionManager).FullName && writeContext.EventId.Name == "FailedDispose"); + } + + using (StartVerifiableLog(expectedErrorsFilter: ExpectedErrors)) + { + var manager = CreateConnectionManager(LoggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = HttpTransportType.LongPolling; + var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory); + var context = MakeRequest("/foo", connection); + var services = new ServiceCollection(); + services.AddSingleton(); + var builder = new ConnectionBuilder(services.BuildServiceProvider()); + builder.UseConnectionHandler(); + var app = builder.Build(); + var options = new HttpConnectionDispatcherOptions(); + // First poll completes immediately + await dispatcher.ExecuteAsync(context, options, app).OrTimeout(); + var sync = new SyncPoint(); + context.Response.Body = new BlockingStream(sync); + var dispatcherTask = dispatcher.ExecuteAsync(context, options, app); + await connection.Transport.Output.WriteAsync(new byte[] { 1 }).OrTimeout(); + await sync.WaitForSyncPoint().OrTimeout(); + // Cancel write to response body + connection.TryCancelSend(long.MaxValue); + sync.Continue(); + await dispatcherTask.OrTimeout(); + // Connection should be removed on canceled write + Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _)); + } + } + + [Fact] + [LogLevel(LogLevel.Debug)] + public async Task SSEConnectionClosesWhenSendTimeoutReached() + { + using (StartVerifiableLog()) + { + var manager = CreateConnectionManager(LoggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = HttpTransportType.ServerSentEvents; + var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory); + var context = MakeRequest("/foo", connection); + SetTransport(context, connection.TransportType); + var services = new ServiceCollection(); + services.AddSingleton(); + var builder = new ConnectionBuilder(services.BuildServiceProvider()); + builder.UseConnectionHandler(); + var app = builder.Build(); + var sync = new SyncPoint(); + context.Response.Body = new BlockingStream(sync, isSSE: true); + var options = new HttpConnectionDispatcherOptions(); + var dispatcherTask = dispatcher.ExecuteAsync(context, options, app); + await connection.Transport.Output.WriteAsync(new byte[] { 1 }).OrTimeout(); + await sync.WaitForSyncPoint().OrTimeout(); + // Cancel write to response body + connection.TryCancelSend(long.MaxValue); + sync.Continue(); + await dispatcherTask.OrTimeout(); + // Connection should be removed on canceled write + Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _)); + } + } + + [Fact] + [LogLevel(LogLevel.Debug)] + public async Task WebSocketConnectionClosesWhenSendTimeoutReached() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(Internal.Transports.WebSocketsServerTransport).FullName && + writeContext.EventId.Name == "ErrorWritingFrame"; + } + using (StartVerifiableLog(expectedErrorsFilter: ExpectedErrors)) + { + var manager = CreateConnectionManager(LoggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = HttpTransportType.WebSockets; + var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory); + var sync = new SyncPoint(); + var context = MakeRequest("/foo", connection); + SetTransport(context, connection.TransportType, sync); + var services = new ServiceCollection(); + services.AddSingleton(); + var builder = new ConnectionBuilder(services.BuildServiceProvider()); + builder.UseConnectionHandler(); + var app = builder.Build(); + var options = new HttpConnectionDispatcherOptions(); + options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0); + var dispatcherTask = dispatcher.ExecuteAsync(context, options, app); + await connection.Transport.Output.WriteAsync(new byte[] { 1 }).OrTimeout(); + await sync.WaitForSyncPoint().OrTimeout(); + // Cancel write to response body + connection.TryCancelSend(long.MaxValue); + sync.Continue(); + await dispatcherTask.OrTimeout(); + // Connection should be removed on canceled write + Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _)); + } + } + [Fact] [LogLevel(LogLevel.Trace)] public async Task WebSocketTransportTimesOutWhenCloseFrameNotReceived() @@ -1622,6 +1794,8 @@ public async Task DeleteEndpointGracefullyTerminatesLongPolling() Assert.Equal(StatusCodes.Status202Accepted, deleteContext.Response.StatusCode); Assert.Equal("text/plain", deleteContext.Response.ContentType); + await connection.DisposeAndRemoveTask.OrTimeout(); + // Verify the connection was removed from the manager Assert.False(manager.TryGetConnection(connection.ConnectionToken, out _)); } @@ -1675,6 +1849,110 @@ public async Task DeleteEndpointGracefullyTerminatesLongPollingEvenWhenBetweenPo } } + [Fact] + public async Task DeleteEndpointTerminatesLongPollingWithHangingApplication() + { + using (StartVerifiableLog()) + { + var manager = CreateConnectionManager(LoggerFactory); + var pipeOptions = new PipeOptions(pauseWriterThreshold: 2, resumeWriterThreshold: 1); + var connection = manager.CreateConnection(pipeOptions, pipeOptions); + connection.TransportType = HttpTransportType.LongPolling; + + var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory); + + var context = MakeRequest("/foo", connection); + + var services = new ServiceCollection(); + services.AddSingleton(); + var builder = new ConnectionBuilder(services.BuildServiceProvider()); + builder.UseConnectionHandler(); + var app = builder.Build(); + var options = new HttpConnectionDispatcherOptions(); + + var pollTask = dispatcher.ExecuteAsync(context, options, app); + Assert.True(pollTask.IsCompleted); + + // Now send the second poll + pollTask = dispatcher.ExecuteAsync(context, options, app); + + // Issue the delete request and make sure the poll completes + var deleteContext = new DefaultHttpContext(); + deleteContext.Request.Path = "/foo"; + deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}"); + deleteContext.Request.Method = "DELETE"; + + Assert.False(pollTask.IsCompleted); + + await dispatcher.ExecuteAsync(deleteContext, options, app).OrTimeout(); + + await pollTask.OrTimeout(); + + // Verify that transport shuts down + await connection.TransportTask.OrTimeout(); + + // Verify the response from the DELETE request + Assert.Equal(StatusCodes.Status202Accepted, deleteContext.Response.StatusCode); + Assert.Equal("text/plain", deleteContext.Response.ContentType); + Assert.Equal(HttpConnectionStatus.Disposed, connection.Status); + + // Verify the connection not removed because application is hanging + Assert.True(manager.TryGetConnection(connection.ConnectionId, out _)); + } + } + + [Fact] + public async Task PollCanReceiveFinalMessageAfterAppCompletes() + { + using (StartVerifiableLog()) + { + var transportType = HttpTransportType.LongPolling; + var manager = CreateConnectionManager(LoggerFactory); + var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = transportType; + + var waitForMessageTcs1 = new TaskCompletionSource(); + var messageTcs1 = new TaskCompletionSource(); + var waitForMessageTcs2 = new TaskCompletionSource(); + var messageTcs2 = new TaskCompletionSource(); + ConnectionDelegate connectionDelegate = async c => + { + await waitForMessageTcs1.Task.OrTimeout(); + await c.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Message1")).OrTimeout(); + messageTcs1.TrySetResult(null); + await waitForMessageTcs2.Task.OrTimeout(); + await c.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Message2")).OrTimeout(); + messageTcs2.TrySetResult(null); + }; + { + var options = new HttpConnectionDispatcherOptions(); + var context = MakeRequest("/foo", connection); + await dispatcher.ExecuteAsync(context, options, connectionDelegate).OrTimeout(); + + // second poll should have data + waitForMessageTcs1.SetResult(null); + await messageTcs1.Task.OrTimeout(); + + var ms = new MemoryStream(); + context.Response.Body = ms; + // Now send the second poll + await dispatcher.ExecuteAsync(context, options, connectionDelegate).OrTimeout(); + Assert.Equal("Message1", Encoding.UTF8.GetString(ms.ToArray())); + + waitForMessageTcs2.SetResult(null); + await messageTcs2.Task.OrTimeout(); + + context = MakeRequest("/foo", connection); + ms.Seek(0, SeekOrigin.Begin); + context.Response.Body = ms; + // This is the third poll which gets the final message after the app is complete + await dispatcher.ExecuteAsync(context, options, connectionDelegate).OrTimeout(); + Assert.Equal("Message2", Encoding.UTF8.GetString(ms.ToArray())); + } + } + } + [Fact] public async Task NegotiateDoesNotReturnWebSocketsWhenNotAvailable() { @@ -1987,12 +2265,12 @@ private static DefaultHttpContext MakeRequest(string path, HttpConnectionContext return context; } - private static void SetTransport(HttpContext context, HttpTransportType transportType) + private static void SetTransport(HttpContext context, HttpTransportType transportType, SyncPoint sync = null) { switch (transportType) { case HttpTransportType.WebSockets: - context.Features.Set(new TestWebSocketConnectionFeature()); + context.Features.Set(new TestWebSocketConnectionFeature(sync)); break; case HttpTransportType.ServerSentEvents: context.Request.Headers["Accept"] = "text/event-stream"; diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs index ade605b08a33..05a29f0e73d1 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs @@ -235,9 +235,6 @@ public async Task CloseConnectionsEndsAllPendingConnections() try { Assert.True(result.IsCompleted); - - // We should be able to write - await connection.Transport.Output.WriteAsync(new byte[] { 1 }); } finally { @@ -248,13 +245,9 @@ public async Task CloseConnectionsEndsAllPendingConnections() connection.TransportTask = Task.Run(async () => { var result = await connection.Application.Input.ReadAsync(); - Assert.Equal(new byte[] { 1 }, result.Buffer.ToArray()); - connection.Application.Input.AdvanceTo(result.Buffer.End); - - result = await connection.Application.Input.ReadAsync(); try { - Assert.True(result.IsCompleted); + Assert.True(result.IsCanceled); } finally { diff --git a/src/SignalR/common/Http.Connections/test/ServerSentEventsMessageFormatterTests.cs b/src/SignalR/common/Http.Connections/test/ServerSentEventsMessageFormatterTests.cs index 2a58e8d4dd88..16407520561f 100644 --- a/src/SignalR/common/Http.Connections/test/ServerSentEventsMessageFormatterTests.cs +++ b/src/SignalR/common/Http.Connections/test/ServerSentEventsMessageFormatterTests.cs @@ -20,7 +20,7 @@ public async Task WriteTextMessageFromSingleSegment(string encoded, string paylo var buffer = new ReadOnlySequence(Encoding.UTF8.GetBytes(payload)); var output = new MemoryStream(); - await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, output); + await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, output, default); Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray())); } @@ -32,7 +32,7 @@ public async Task WriteTextMessageFromMultipleSegments(string encoded, string pa var buffer = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(Encoding.UTF8.GetBytes(payload)); var output = new MemoryStream(); - await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, output); + await ServerSentEventsMessageFormatter.WriteMessageAsync(buffer, output, default); Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray())); } diff --git a/src/SignalR/common/Http.Connections/test/TestWebSocketConnectionFeature.cs b/src/SignalR/common/Http.Connections/test/TestWebSocketConnectionFeature.cs index f67dd940031e..9bbb6894dbe4 100644 --- a/src/SignalR/common/Http.Connections/test/TestWebSocketConnectionFeature.cs +++ b/src/SignalR/common/Http.Connections/test/TestWebSocketConnectionFeature.cs @@ -5,11 +5,21 @@ using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR.Tests; namespace Microsoft.AspNetCore.Http.Connections.Tests { internal class TestWebSocketConnectionFeature : IHttpWebSocketFeature, IDisposable { + public TestWebSocketConnectionFeature() + { } + public TestWebSocketConnectionFeature(SyncPoint sync) + { + _sync = sync; + } + + private readonly SyncPoint _sync; private readonly TaskCompletionSource _accepted = new TaskCompletionSource(); public bool IsWebSocketRequest => true; @@ -27,8 +37,8 @@ public Task AcceptAsync(WebSocketAcceptContext context) var clientToServer = Channel.CreateUnbounded(); var serverToClient = Channel.CreateUnbounded(); - var clientSocket = new WebSocketChannel(serverToClient.Reader, clientToServer.Writer); - var serverSocket = new WebSocketChannel(clientToServer.Reader, serverToClient.Writer); + var clientSocket = new WebSocketChannel(serverToClient.Reader, clientToServer.Writer, _sync); + var serverSocket = new WebSocketChannel(clientToServer.Reader, serverToClient.Writer, _sync); Client = clientSocket; SubProtocol = context.SubProtocol; @@ -45,16 +55,18 @@ public class WebSocketChannel : WebSocket { private readonly ChannelReader _input; private readonly ChannelWriter _output; + private readonly SyncPoint _sync; private WebSocketCloseStatus? _closeStatus; private string _closeStatusDescription; private WebSocketState _state; private WebSocketMessage _internalBuffer = new WebSocketMessage(); - public WebSocketChannel(ChannelReader input, ChannelWriter output) + public WebSocketChannel(ChannelReader input, ChannelWriter output, SyncPoint sync = null) { _input = input; _output = output; + _sync = sync; } public override WebSocketCloseStatus? CloseStatus => _closeStatus; @@ -173,11 +185,17 @@ public override async Task ReceiveAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { + if (_sync != null) + { + await _sync.WaitToContinue(); + } + cancellationToken.ThrowIfCancellationRequested(); + var copy = new byte[buffer.Count]; Buffer.BlockCopy(buffer.Array, buffer.Offset, copy, 0, buffer.Count); - return SendMessageAsync(new WebSocketMessage + await SendMessageAsync(new WebSocketMessage { Buffer = copy, MessageType = messageType, diff --git a/src/SignalR/common/Shared/PipeWriterStream.cs b/src/SignalR/common/Shared/PipeWriterStream.cs index 518daa656fc5..2afdc723d2a8 100644 --- a/src/SignalR/common/Shared/PipeWriterStream.cs +++ b/src/SignalR/common/Shared/PipeWriterStream.cs @@ -74,7 +74,16 @@ private ValueTask WriteCoreAsync(ReadOnlyMemory source, CancellationToken _length += source.Length; var task = _pipeWriter.WriteAsync(source); - if (!task.IsCompletedSuccessfully) + + if (task.IsCompletedSuccessfully) + { + // Cancellation can be triggered by PipeWriter.CancelPendingFlush + if (task.Result.IsCanceled) + { + throw new OperationCanceledException(); + } + } + else { return WriteSlowAsync(task); } diff --git a/src/SignalR/common/testassets/Tests.Utils/TestClient.cs b/src/SignalR/common/testassets/Tests.Utils/TestClient.cs index 6183691ad46f..ddb7dee2018a 100644 --- a/src/SignalR/common/testassets/Tests.Utils/TestClient.cs +++ b/src/SignalR/common/testassets/Tests.Utils/TestClient.cs @@ -37,9 +37,10 @@ class TestClient : ITransferFormatFeature, IConnectionHeartbeatFeature, IDisposa public TransferFormat ActiveFormat { get; set; } - public TestClient(IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, string userIdentifier = null) + public TestClient(IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, string userIdentifier = null, long pauseWriterThreshold = 32768) { - var options = new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); + var options = new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false, + pauseWriterThreshold: pauseWriterThreshold, resumeWriterThreshold: pauseWriterThreshold / 2); var pair = DuplexPipe.CreateConnectionPair(options, options); Connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Transport, pair.Application); @@ -70,16 +71,7 @@ public async Task ConnectAsync( { if (sendHandshakeRequestMessage) { - var memoryBufferWriter = MemoryBufferWriter.Get(); - try - { - HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name, _protocol.Version), memoryBufferWriter); - await Connection.Application.Output.WriteAsync(memoryBufferWriter.ToArray()); - } - finally - { - MemoryBufferWriter.Return(memoryBufferWriter); - } + await Connection.Application.Output.WriteAsync(GetHandshakeRequestMessage()); } var connection = handler.OnConnectedAsync(Connection); @@ -257,7 +249,7 @@ public HubMessage TryRead(bool isHandshake = false) } else { - // read first message out of the incoming data + // read first message out of the incoming data if (HandshakeProtocol.TryParseResponseMessage(ref buffer, out var responseMessage)) { return responseMessage; @@ -312,6 +304,20 @@ public void TickHeartbeat() } } + public byte[] GetHandshakeRequestMessage() + { + var memoryBufferWriter = MemoryBufferWriter.Get(); + try + { + HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name, _protocol.Version), memoryBufferWriter); + return memoryBufferWriter.ToArray(); + } + finally + { + MemoryBufferWriter.Return(memoryBufferWriter); + } + } + private class DefaultInvocationBinder : IInvocationBinder { public IReadOnlyList GetParameterTypes(string methodName) diff --git a/src/SignalR/perf/Microbenchmarks/ServerSentEventsBenchmark.cs b/src/SignalR/perf/Microbenchmarks/ServerSentEventsBenchmark.cs index fd4357c95247..5b20e7209d64 100644 --- a/src/SignalR/perf/Microbenchmarks/ServerSentEventsBenchmark.cs +++ b/src/SignalR/perf/Microbenchmarks/ServerSentEventsBenchmark.cs @@ -61,7 +61,7 @@ public void GlobalSetup() _parser = new ServerSentEventsMessageParser(); _rawData = new ReadOnlySequence(protocol.GetMessageBytes(hubMessage)); var ms = new MemoryStream(); - ServerSentEventsMessageFormatter.WriteMessageAsync(_rawData, ms).GetAwaiter().GetResult(); + ServerSentEventsMessageFormatter.WriteMessageAsync(_rawData, ms, default).GetAwaiter().GetResult(); _sseFormattedData = ms.ToArray(); } @@ -81,7 +81,7 @@ public void ReadSingleMessage() [Benchmark] public Task WriteSingleMessage() { - return ServerSentEventsMessageFormatter.WriteMessageAsync(_rawData, Stream.Null); + return ServerSentEventsMessageFormatter.WriteMessageAsync(_rawData, Stream.Null, default); } public enum Message diff --git a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs index 3c835ab933e2..02609bce1ecf 100644 --- a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs +++ b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs @@ -82,10 +82,10 @@ public override Task RemoveFromGroupAsync(string connectionId, string groupName, /// public override Task SendAllAsync(string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, null); + return SendToAllConnections(methodName, args, include: null, cancellationToken); } - private Task SendToAllConnections(string methodName, object[] args, Func include) + private Task SendToAllConnections(string methodName, object[] args, Func include, CancellationToken cancellationToken) { List tasks = null; SerializedHubMessage message = null; @@ -103,7 +103,7 @@ private Task SendToAllConnections(string methodName, object[] args, Func connections, Func include, ref List tasks, ref SerializedHubMessage message) + private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary connections, Func include, + ref List tasks, ref SerializedHubMessage message, CancellationToken cancellationToken) { // foreach over ConcurrentDictionary avoids allocating an enumerator foreach (var connection in connections) @@ -142,7 +143,7 @@ private void SendToGroupConnections(string methodName, object[] args, Concurrent message = CreateSerializedInvocationMessage(methodName, args); } - var task = connection.Value.WriteAsync(message); + var task = connection.Value.WriteAsync(message, cancellationToken); if (!task.IsCompletedSuccessfully) { @@ -175,7 +176,7 @@ public override Task SendConnectionAsync(string connectionId, string methodName, // Write message directly to connection without caching it in memory var message = CreateInvocationMessage(methodName, args); - return connection.WriteAsync(message).AsTask(); + return connection.WriteAsync(message, cancellationToken).AsTask(); } /// @@ -193,7 +194,7 @@ public override Task SendGroupAsync(string groupName, string methodName, object[ // group might be modified inbetween checking and sending List tasks = null; SerializedHubMessage message = null; - SendToGroupConnections(methodName, args, group, null, ref tasks, ref message); + SendToGroupConnections(methodName, args, group, null, ref tasks, ref message, cancellationToken); if (tasks != null) { @@ -221,7 +222,7 @@ public override Task SendGroupsAsync(IReadOnlyList groupNames, string me var group = _groups[groupName]; if (group != null) { - SendToGroupConnections(methodName, args, group, null, ref tasks, ref message); + SendToGroupConnections(methodName, args, group, null, ref tasks, ref message, cancellationToken); } } @@ -247,7 +248,7 @@ public override Task SendGroupExceptAsync(string groupName, string methodName, o List tasks = null; SerializedHubMessage message = null; - SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message); + SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message, cancellationToken); if (tasks != null) { @@ -271,7 +272,7 @@ private HubMessage CreateInvocationMessage(string methodName, object[] args) /// public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => string.Equals(connection.UserIdentifier, userId, StringComparison.Ordinal)); + return SendToAllConnections(methodName, args, connection => string.Equals(connection.UserIdentifier, userId, StringComparison.Ordinal), cancellationToken); } /// @@ -292,19 +293,19 @@ public override Task OnDisconnectedAsync(HubConnectionContext connection) /// public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => !excludedConnectionIds.Contains(connection.ConnectionId)); + return SendToAllConnections(methodName, args, connection => !excludedConnectionIds.Contains(connection.ConnectionId), cancellationToken); } /// public override Task SendConnectionsAsync(IReadOnlyList connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => connectionIds.Contains(connection.ConnectionId)); + return SendToAllConnections(methodName, args, connection => connectionIds.Contains(connection.ConnectionId), cancellationToken); } /// public override Task SendUsersAsync(IReadOnlyList userIds, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => userIds.Contains(connection.UserIdentifier)); + return SendToAllConnections(methodName, args, connection => userIds.Contains(connection.UserIdentifier), cancellationToken); } } } diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index 8e9216d35d80..11e05c177c9c 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -34,6 +34,8 @@ public class HubConnectionContext private readonly long _keepAliveInterval; private readonly long _clientTimeoutInterval; private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1); + private readonly bool _useAbsoluteClientTimeout; + private readonly object _receiveMessageTimeoutLock = new object(); private StreamTracker _streamTracker; private long _lastSendTimeStamp = DateTime.UtcNow.Ticks; @@ -41,10 +43,13 @@ public class HubConnectionContext private bool _receivedMessageThisInterval = false; private ReadOnlyMemory _cachedPingMessage; private bool _clientTimeoutActive; - private bool _connectionAborted; + private volatile bool _connectionAborted; private volatile bool _allowReconnect = true; private int _streamBufferCapacity; private long? _maxMessageSize; + private bool _receivedMessageTimeoutEnabled = false; + private long _receivedMessageElapsedTicks = 0; + private long _receivedMessageTimestamp; /// /// Initializes a new instance of the class. @@ -64,6 +69,11 @@ public HubConnectionContext(ConnectionContext connectionContext, HubConnectionCo ConnectionAborted = _connectionAbortedTokenSource.Token; HubCallerContext = new DefaultHubCallerContext(this); + + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.UseAbsoluteClientTimeout", out var useAbsoluteClientTimeout)) + { + _useAbsoluteClientTimeout = useAbsoluteClientTimeout; + } } internal StreamTracker StreamTracker @@ -131,7 +141,7 @@ public virtual ValueTask WriteAsync(HubMessage message, CancellationToken cancel // Try to grab the lock synchronously, if we fail, go to the slower path if (!_writeLock.Wait(0)) { - return new ValueTask(WriteSlowAsync(message)); + return new ValueTask(WriteSlowAsync(message, cancellationToken)); } if (_connectionAborted) @@ -141,7 +151,7 @@ public virtual ValueTask WriteAsync(HubMessage message, CancellationToken cancel } // This method should never throw synchronously - var task = WriteCore(message); + var task = WriteCore(message, cancellationToken); // The write didn't complete synchronously so await completion if (!task.IsCompletedSuccessfully) @@ -167,7 +177,7 @@ public virtual ValueTask WriteAsync(SerializedHubMessage message, CancellationTo // Try to grab the lock synchronously, if we fail, go to the slower path if (!_writeLock.Wait(0)) { - return new ValueTask(WriteSlowAsync(message)); + return new ValueTask(WriteSlowAsync(message, cancellationToken)); } if (_connectionAborted) @@ -177,7 +187,7 @@ public virtual ValueTask WriteAsync(SerializedHubMessage message, CancellationTo } // This method should never throw synchronously - var task = WriteCore(message); + var task = WriteCore(message, cancellationToken); // The write didn't complete synchronously so await completion if (!task.IsCompletedSuccessfully) @@ -191,7 +201,7 @@ public virtual ValueTask WriteAsync(SerializedHubMessage message, CancellationTo return default; } - private ValueTask WriteCore(HubMessage message) + private ValueTask WriteCore(HubMessage message, CancellationToken cancellationToken) { try { @@ -199,7 +209,7 @@ private ValueTask WriteCore(HubMessage message) // write it without caching. Protocol.WriteMessage(message, _connectionContext.Transport.Output); - return _connectionContext.Transport.Output.FlushAsync(); + return _connectionContext.Transport.Output.FlushAsync(cancellationToken); } catch (Exception ex) { @@ -211,14 +221,14 @@ private ValueTask WriteCore(HubMessage message) } } - private ValueTask WriteCore(SerializedHubMessage message) + private ValueTask WriteCore(SerializedHubMessage message, CancellationToken cancellationToken) { try { // Grab a preserialized buffer for this protocol. var buffer = message.GetSerializedMessage(Protocol); - return _connectionContext.Transport.Output.WriteAsync(buffer); + return _connectionContext.Transport.Output.WriteAsync(buffer, cancellationToken); } catch (Exception ex) { @@ -249,10 +259,10 @@ private async Task CompleteWriteAsync(ValueTask task) } } - private async Task WriteSlowAsync(HubMessage message) + private async Task WriteSlowAsync(HubMessage message, CancellationToken cancellationToken) { // Failed to get the lock immediately when entering WriteAsync so await until it is available - await _writeLock.WaitAsync(); + await _writeLock.WaitAsync(cancellationToken); try { @@ -261,7 +271,7 @@ private async Task WriteSlowAsync(HubMessage message) return; } - await WriteCore(message); + await WriteCore(message, cancellationToken); } catch (Exception ex) { @@ -274,7 +284,7 @@ private async Task WriteSlowAsync(HubMessage message) } } - private async Task WriteSlowAsync(SerializedHubMessage message) + private async Task WriteSlowAsync(SerializedHubMessage message, CancellationToken cancellationToken) { // Failed to get the lock immediately when entering WriteAsync so await until it is available await _writeLock.WaitAsync(); @@ -286,7 +296,7 @@ private async Task WriteSlowAsync(SerializedHubMessage message) return; } - await WriteCore(message); + await WriteCore(message, cancellationToken); } catch (Exception ex) { @@ -370,6 +380,9 @@ public virtual void Abort() private void AbortAllowReconnect() { _connectionAborted = true; + // Cancel any current writes or writes that are about to happen and have already gone past the _connectionAborted bool + // We have to do this outside of the lock otherwise it could hang if the write is observing backpressure + _connectionContext.Transport.Output.CancelPendingFlush(); // If we already triggered the token then noop, this isn't thread safe but it's good enough // to avoid spawning a new task in the most common cases @@ -525,9 +538,23 @@ await WriteHandshakeResponseAsync(new HandshakeResponseMessage( internal Task AbortAsync() { AbortAllowReconnect(); + + // Acquire lock to make sure all writes are completed + if (!_writeLock.Wait(0)) + { + return AbortAsyncSlow(); + } + _writeLock.Release(); return _abortCompletedTcs.Task; } + private async Task AbortAsyncSlow() + { + await _writeLock.WaitAsync(); + _writeLock.Release(); + await _abortCompletedTcs.Task; + } + private void KeepAliveTick() { var currentTime = DateTime.UtcNow.Ticks; @@ -564,17 +591,41 @@ internal void StartClientTimeout() private void CheckClientTimeout() { - // If it's been too long since we've heard from the client, then close this - if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval) + if (Debugger.IsAttached) { - if (!_receivedMessageThisInterval) + return; + } + + if (_useAbsoluteClientTimeout) + { + // If it's been too long since we've heard from the client, then close this + if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval) { - Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); - AbortAllowReconnect(); + if (!_receivedMessageThisInterval) + { + Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); + AbortAllowReconnect(); + } + + _receivedMessageThisInterval = false; + Volatile.Write(ref _lastReceivedTimeStamp, DateTime.UtcNow.Ticks); } + } + else + { + lock (_receiveMessageTimeoutLock) + { + if (_receivedMessageTimeoutEnabled) + { + _receivedMessageElapsedTicks = DateTime.UtcNow.Ticks - _receivedMessageTimestamp; - _receivedMessageThisInterval = false; - Volatile.Write(ref _lastReceivedTimeStamp, DateTime.UtcNow.Ticks); + if (_receivedMessageElapsedTicks >= _clientTimeoutInterval) + { + Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); + AbortAllowReconnect(); + } + } + } } } @@ -623,6 +674,35 @@ internal void ResetClientTimeout() _receivedMessageThisInterval = true; } + internal void BeginClientTimeout() + { + // check if new timeout behavior is in use + if (!_useAbsoluteClientTimeout) + { + lock (_receiveMessageTimeoutLock) + { + _receivedMessageTimeoutEnabled = true; + _receivedMessageTimestamp = DateTime.UtcNow.Ticks; + } + } + } + + internal void StopClientTimeout() + { + // check if new timeout behavior is in use + if (!_useAbsoluteClientTimeout) + { + lock (_receiveMessageTimeoutLock) + { + // we received a message so stop the timer and reset it + // it will resume after the message has been processed + _receivedMessageElapsedTicks = 0; + _receivedMessageTimestamp = 0; + _receivedMessageTimeoutEnabled = false; + } + } + } + private static class Log { // Category: HubConnectionContext diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 663864cbb99e..0a8f3380f91c 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -213,6 +213,8 @@ private async Task DispatchMessagesAsync(HubConnectionContext connection) { var input = connection.Input; var protocol = connection.Protocol; + connection.BeginClientTimeout(); + var binder = new HubConnectionBinder(_dispatcher, connection); @@ -221,6 +223,8 @@ private async Task DispatchMessagesAsync(HubConnectionContext connection) var result = await input.ReadAsync(); var buffer = result.Buffer; + connection.ResetClientTimeout(); + try { if (result.IsCanceled) @@ -230,15 +234,21 @@ private async Task DispatchMessagesAsync(HubConnectionContext connection) if (!buffer.IsEmpty) { - connection.ResetClientTimeout(); - + bool messageReceived = false; // No message limit, just parse and dispatch if (_maximumMessageSize == null) { while (protocol.TryParseMessage(ref buffer, binder, out var message)) { + messageReceived = true; + connection.StopClientTimeout(); await _dispatcher.DispatchMessageAsync(connection, message); } + + if (messageReceived) + { + connection.BeginClientTimeout(); + } } else { @@ -258,6 +268,9 @@ private async Task DispatchMessagesAsync(HubConnectionContext connection) if (protocol.TryParseMessage(ref segment, binder, out var message)) { + messageReceived = true; + connection.StopClientTimeout(); + await _dispatcher.DispatchMessageAsync(connection, message); } else if (overLength) @@ -273,6 +286,11 @@ private async Task DispatchMessagesAsync(HubConnectionContext connection) // Update the buffer to the remaining segment buffer = buffer.Slice(segment.Start); } + + if (messageReceived) + { + connection.BeginClientTimeout(); + } } } diff --git a/src/SignalR/server/Core/src/Internal/Proxies.cs b/src/SignalR/server/Core/src/Internal/Proxies.cs index 9a3edd56bdb5..8a2beb26de03 100644 --- a/src/SignalR/server/Core/src/Internal/Proxies.cs +++ b/src/SignalR/server/Core/src/Internal/Proxies.cs @@ -105,7 +105,7 @@ public AllClientProxy(HubLifetimeManager lifetimeManager) public Task SendCoreAsync(string method, object[] args, CancellationToken cancellationToken = default) { - return _lifetimeManager.SendAllAsync(method, args); + return _lifetimeManager.SendAllAsync(method, args, cancellationToken); } } diff --git a/src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs b/src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs index 0e00c9a9ab54..ee312dbf3e66 100644 --- a/src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs +++ b/src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs @@ -1,9 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.AspNetCore.SignalR.Specification.Tests; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.AspNetCore.SignalR.Specification.Tests; +using Xunit; namespace Microsoft.AspNetCore.SignalR.Tests { @@ -13,5 +18,241 @@ public override HubLifetimeManager CreateNewHubLifetimeManager() { return new DefaultHubLifetimeManager(new Logger>(NullLoggerFactory.Instance)); } + + [Fact] + public async Task SendAllAsyncWillCancelWithToken() + { + using (var client1 = new TestClient()) + using (var client2 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + var connection2 = HubConnectionContextUtils.Create(client2.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.OnConnectedAsync(connection2).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendAllAsync("Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var message = Assert.IsType(client1.TryRead()); + Assert.Equal("Hello", message.Target); + Assert.Single(message.Arguments); + Assert.Equal("World", (string)message.Arguments[0]); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection2.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + Assert.False(connection1.ConnectionAborted.IsCancellationRequested); + } + } + + [Fact] + public async Task SendAllExceptAsyncWillCancelWithToken() + { + using (var client1 = new TestClient()) + using (var client2 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + var connection2 = HubConnectionContextUtils.Create(client2.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.OnConnectedAsync(connection2).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendAllExceptAsync("Hello", new object[] { "World" }, new List { connection1.ConnectionId }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection2.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + Assert.False(connection1.ConnectionAborted.IsCancellationRequested); + Assert.Null(client1.TryRead()); + } + } + + [Fact] + public async Task SendConnectionAsyncWillCancelWithToken() + { + using (var client1 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendConnectionAsync(connection1.ConnectionId, "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection1.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + } + } + + [Fact] + public async Task SendConnectionsAsyncWillCancelWithToken() + { + using (var client1 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendConnectionsAsync(new List { connection1.ConnectionId }, "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection1.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + } + } + + [Fact] + public async Task SendGroupAsyncWillCancelWithToken() + { + using (var client1 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendGroupAsync("group", "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection1.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + } + } + + [Fact] + public async Task SendGroupExceptAsyncWillCancelWithToken() + { + using (var client1 = new TestClient()) + using (var client2 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + var connection2 = HubConnectionContextUtils.Create(client2.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.OnConnectedAsync(connection2).OrTimeout(); + await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); + await manager.AddToGroupAsync(connection2.ConnectionId, "group").OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendGroupExceptAsync("group", "Hello", new object[] { "World" }, new List { connection1.ConnectionId }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection2.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + Assert.False(connection1.ConnectionAborted.IsCancellationRequested); + Assert.Null(client1.TryRead()); + } + } + + [Fact] + public async Task SendGroupsAsyncWillCancelWithToken() + { + using (var client1 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendGroupsAsync(new List { "group" }, "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection1.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + } + } + + [Fact] + public async Task SendUserAsyncWillCancelWithToken() + { + using (var client1 = new TestClient()) + using (var client2 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection, userIdentifier: "user"); + var connection2 = HubConnectionContextUtils.Create(client2.Connection, userIdentifier: "user"); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.OnConnectedAsync(connection2).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendUserAsync("user", "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var message = Assert.IsType(client1.TryRead()); + Assert.Equal("Hello", message.Target); + Assert.Single(message.Arguments); + Assert.Equal("World", (string)message.Arguments[0]); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection2.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + Assert.False(connection1.ConnectionAborted.IsCancellationRequested); + } + } + + [Fact] + public async Task SendUsersAsyncWillCancelWithToken() + { + using (var client1 = new TestClient()) + using (var client2 = new TestClient(pauseWriterThreshold: 2)) + { + var manager = CreateNewHubLifetimeManager(); + var connection1 = HubConnectionContextUtils.Create(client1.Connection, userIdentifier: "user1"); + var connection2 = HubConnectionContextUtils.Create(client2.Connection, userIdentifier: "user2"); + await manager.OnConnectedAsync(connection1).OrTimeout(); + await manager.OnConnectedAsync(connection2).OrTimeout(); + var cts = new CancellationTokenSource(); + var sendTask = manager.SendUsersAsync(new List { "user1", "user2" }, "Hello", new object[] { "World" }, cts.Token).OrTimeout(); + Assert.False(sendTask.IsCompleted); + cts.Cancel(); + await sendTask.OrTimeout(); + var message = Assert.IsType(client1.TryRead()); + Assert.Equal("Hello", message.Target); + Assert.Single(message.Arguments); + Assert.Equal("World", (string)message.Arguments[0]); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection2.ConnectionAborted.Register(t => + { + ((TaskCompletionSource)t).SetResult(null); + }, tcs); + await tcs.Task.OrTimeout(); + Assert.False(connection1.ConnectionAborted.IsCancellationRequested); + } + } } } diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 1e68a1f201e6..064aeb8ebc13 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -2797,6 +2797,47 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring() } } + [Fact] + public async Task HubMethodInvokeDoesNotCountTowardsClientTimeout() + { + using (StartVerifiableLog()) + { + var tcsService = new TcsService(); + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.Configure(options => + options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(0)); + services.AddSingleton(tcsService); + }, LoggerFactory); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient(new JsonHubProtocol())) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + // This starts the timeout logic + await client.SendHubMessageAsync(PingMessage.Instance); + + // Call long running hub method + var hubMethodTask = client.InvokeAsync(nameof(LongRunningHub.LongRunningMethod)); + await tcsService.StartedMethod.Task.OrTimeout(); + + // Tick heartbeat while hub method is running to show that close isn't triggered + client.TickHeartbeat(); + + // Unblock long running hub method + tcsService.EndMethod.SetResult(null); + + await hubMethodTask.OrTimeout(); + + // Tick heartbeat again now that we're outside of the hub method + client.TickHeartbeat(); + + // Connection is closed + await connectionHandlerTask.OrTimeout(); + } + } + } + [Fact] public async Task EndingConnectionSendsCloseMessageWithNoError() {