diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index caa6557d49..67d59a247f 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -29,7 +29,9 @@ variables: - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}: - name: _BuildArgs value: '' + jobs: +# Code check - template: jobs/default-build.yml parameters: jobName: Code_check @@ -38,6 +40,10 @@ jobs: steps: - powershell: ./eng/scripts/CodeCheck.ps1 -ci displayName: Run eng/scripts/CodeCheck.ps1 + artifacts: + - name: Code_Check_Logs + path: artifacts/log/ + publishOnError: true # Build Windows (x64/x86) - template: jobs/default-build.yml @@ -171,6 +177,7 @@ jobs: -bl:artifacts/log/build.macos.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: MacOS_x64_Packages path: artifacts/packages/ @@ -191,7 +198,6 @@ jobs: jobName: Linux_x64_build jobDisplayName: "Build: Linux x64" agentOs: Linux - installNodeJs: false steps: - script: ./build.sh --ci @@ -211,6 +217,7 @@ jobs: --arch x64 \ --build-installers \ --no-build-deps \ + --no-build-nodejs \ -p:OnlyPackPlatformSpecificPackages=true \ -p:BuildRuntimeArchive=false \ -p:LinuxInstallerType=deb \ @@ -224,12 +231,15 @@ jobs: --arch x64 \ --build-installers \ --no-build-deps \ + --no-build-nodejs \ -p:OnlyPackPlatformSpecificPackages=true \ -p:BuildRuntimeArchive=false \ -p:LinuxInstallerType=rpm \ -bl:artifacts/log/build.rpm.binlog \ $(_BuildArgs) displayName: Build RPM installers + installNodeJs: false + installJdk: false artifacts: - name: Linux_x64_Packages path: artifacts/packages/ @@ -260,6 +270,7 @@ jobs: -bl:artifacts/log/build.linux-arm.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_arm_Packages path: artifacts/packages/ @@ -290,6 +301,7 @@ jobs: -bl:artifacts/log/build.arm64.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_arm64_Packages path: artifacts/packages/ @@ -323,6 +335,7 @@ jobs: -bl:artifacts/log/build.musl.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_musl_x64_Packages path: artifacts/packages/ @@ -337,7 +350,7 @@ jobs: parameters: inputName: Linux_musl_x64 -# Build Linux Musl arm64 +# Build Linux Musl ARM64 - template: jobs/default-build.yml parameters: jobName: Linux_musl_arm64_build @@ -356,6 +369,7 @@ jobs: -bl:artifacts/log/build.musl.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_musl_arm64_Packages path: artifacts/packages/ @@ -499,7 +513,7 @@ jobs: version: 3.0.x installationPath: $(DotNetCoreSdkDir) includePreviewVersions: true - - script: ./eng/scripts/ci-source-build.sh --ci --configuration Release /p:BuildManaged=true + - 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 displayName: Upload logs diff --git a/NuGet.config b/NuGet.config index eda7e2c3e9..61a7c1b08c 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,13 +1,18 @@ + - + + + + + diff --git a/build.ps1 b/build.ps1 index 018c3970b7..17020044ed 100644 --- a/build.ps1 +++ b/build.ps1 @@ -183,7 +183,7 @@ elseif ($Projects) { } # When adding new sub-group build flags, add them to this check. elseif((-not $BuildNative) -and (-not $BuildManaged) -and (-not $BuildNodeJS) -and (-not $BuildInstallers) -and (-not $BuildJava)) { - Write-Warning "No default group of projects was specified, so building the 'managed' subsets of projects. Run ``build.cmd -help`` for more details." + Write-Warning "No default group of projects was specified, so building the 'managed' and its dependent subsets of projects. Run ``build.cmd -help`` for more details." # This goal of this is to pick a sensible default for `build.cmd` with zero arguments. # Now that we support subfolder invokations of build.cmd, we will be pushing to have build.cmd build everything (-all) by default @@ -191,6 +191,25 @@ elseif((-not $BuildNative) -and (-not $BuildManaged) -and (-not $BuildNodeJS) -a $BuildManaged = $true } +if ($BuildManaged -or ($All -and (-not $NoBuildManaged))) { + if ((-not $BuildNodeJS) -and (-not $NoBuildNodeJS)) { + $node = Get-Command node -ErrorAction Ignore -CommandType Application + + if ($node) { + $nodeHome = Split-Path -Parent (Split-Path -Parent $node.Path) + Write-Host -f Magenta "Building of C# project is enabled and has dependencies on NodeJS projects. Building of NodeJS projects is enabled since node is detected in $nodeHome." + } + else { + Write-Host -f Magenta "Building of NodeJS projects is disabled since node is not detected on Path and no BuildNodeJs or NoBuildNodeJs setting is set explicitly." + $NoBuildNodeJS = $true + } + } + + if ($NoBuildNodeJS){ + Write-Warning "Some managed projects depend on NodeJS projects. Building NodeJS is disabled so the managed projects will fallback to using the output from previous builds. The output may not be correct or up to date." + } +} + if ($BuildInstallers) { $MSBuildArguments += "/p:BuildInstallers=true" } if ($BuildManaged) { $MSBuildArguments += "/p:BuildManaged=true" } if ($BuildNative) { $MSBuildArguments += "/p:BuildNative=true" } diff --git a/build.sh b/build.sh index c170ac1844..ad4ce2c1c8 100755 --- a/build.sh +++ b/build.sh @@ -213,7 +213,7 @@ elif [ ! -z "$build_projects" ]; then elif [ -z "$build_managed" ] && [ -z "$build_nodejs" ] && [ -z "$build_java" ] && [ -z "$build_native" ] && [ -z "$build_installers" ]; then # This goal of this is to pick a sensible default for `build.sh` with zero arguments. # We believe the most common thing our contributors will work on is C#, so if no other build group was picked, build the C# projects. - __warn "No default group of projects was specified, so building the 'managed' subset of projects. Run ``build.sh --help`` for more details." + __warn "No default group of projects was specified, so building the 'managed' and its dependent subset of projects. Run ``build.sh --help`` for more details." build_managed=true fi @@ -221,6 +221,21 @@ if [ "$build_deps" = false ]; then msbuild_args[${#msbuild_args[*]}]="-p:BuildProjectReferences=false" fi +if [ "$build_managed" = true ] || (["$build_all" = true ] && [ "$build_managed" != false ]); then + if [ -z "$build_nodejs" ]; then + if [ -x "$(command -v node)" ]; then + __warn "Building of C# project is enabled and has dependencies on NodeJS projects. Building of NodeJS projects is enabled since node is detected on PATH." + else + __warn "Building of NodeJS projects is disabled since node is not detected on Path and no BuildNodeJs or NoBuildNodeJs setting is set explicitly." + build_nodejs=false + fi + fi + + if [ "$build_nodejs" = false ]; then + __warn "Some managed projects depend on NodeJS projects. Building NodeJS is disabled so the managed projects will fallback to using the output from previous builds. The output may not be correct or up to date." + fi +fi + # Only set these MSBuild properties if they were explicitly set by build parameters. [ ! -z "$build_java" ] && msbuild_args[${#msbuild_args[*]}]="-p:BuildJava=$build_java" [ ! -z "$build_native" ] && msbuild_args[${#msbuild_args[*]}]="-p:BuildNative=$build_native" diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index 53938c189b..7740ec6140 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -97,14 +97,14 @@ The cause of this problem is that the solution you are using does not include th ``` ### Common error: Unable to locate the .NET Core SDK - + Executing `.\restore.cmd` or `.\build.cmd` may produce these errors: > error : Unable to locate the .NET Core SDK. Check that it is installed and that the version specified in global.json (if any) matches the installed version. > error MSB4236: The SDK 'Microsoft.NET.Sdk' specified could not be found. In most cases, this is because the option _Use previews of the .NET Core SDK_ in VS2019 is not checked. Start Visual Studio, go to _Tools > Options_ and check _Use previews of the .NET Core SDK_ under _Environment > Preview Features_. - + ## Building with Visual Studio Code Using Visual Studio Code with this repo requires setting environment variables on command line first. @@ -138,6 +138,8 @@ On macOS/Linux: ./build.sh ``` +By default, all of the C# projects are built. Some C# projects requires NodeJS to be installed to compile JavaScript assets which are then checked in as source. If NodeJS is detected on the path, the NodeJS projects will be compiled as part of building C# projects. If NodeJS is not detected on the path, the JavaScript assets checked in previously will be used instead. To disable building NodeJS projects, specify /p:BuildNodeJs=false on the command line. + ### Using `dotnet` on command line in this repo Because we are using pre-release versions of .NET Core, you have to set a handful of environment variables diff --git a/eng/Build.props b/eng/Build.props index eba7a2ac91..ef1a7776b6 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -4,14 +4,16 @@ - true - true - true - true + true + true + true + true + + - @@ -102,6 +104,7 @@ https://github.com/aspnet/AspNetCore-Tooling 448a88e86d20fd9315901f663318d64c9c6841bf - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe https://github.com/dotnet/corefx @@ -412,25 +412,25 @@ https://github.com/dotnet/corefx 80f411d58df8338ccd9430900b541a037a9cb383 - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe https://github.com/dotnet/roslyn diff --git a/eng/Versions.props b/eng/Versions.props index 3fcb044a1f..48199dc488 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -55,7 +55,7 @@ --> - 1.0.0-beta.19369.2 + 1.0.0-beta.19404.1 3.3.0-beta3-19401-01 @@ -92,80 +92,87 @@ 5.0.0-alpha1.19405.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 +<<<<<<< HEAD 5.0.0-alpha1.19407.1 5.0.0-alpha1.19407.1 5.0.0-alpha1.19407.1 5.0.0-alpha1.19407.1 +======= + 3.0.0-preview9.19405.6 + 3.0.0-preview9.19405.6 + 3.0.0-preview9.19405.6 + 3.0.0-preview9.19405.6 +>>>>>>> release/3.0 - - $(RestoreSources); - https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json; - https://grpc.jfrog.io/grpc/api/nuget/v3/grpc-nuget-dev; - - - $(RestoreSources); - https://dotnet.myget.org/F/roslyn/api/v3/index.json; - - - - $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; - - + https://dotnetcli.blob.core.windows.net/dotnet/ diff --git a/eng/Workarounds.props b/eng/Workarounds.props index 0fa6edaeab..a56cf44b46 100644 --- a/eng/Workarounds.props +++ b/eng/Workarounds.props @@ -8,25 +8,6 @@ portable - - - $(RepoRoot)NuGet.config - - - - - - $(RestoreSources); - https://dotnet.myget.org/F/roslyn-tools/api/v3/index.json; - - - false diff --git a/eng/Workarounds.targets b/eng/Workarounds.targets index ed0324f744..f5c74cdd43 100644 --- a/eng/Workarounds.targets +++ b/eng/Workarounds.targets @@ -29,12 +29,6 @@ - - - $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; - - diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 index 9d18645f45..8cf18bcfeb 100644 --- a/eng/common/init-tools-native.ps1 +++ b/eng/common/init-tools-native.ps1 @@ -98,12 +98,18 @@ try { } Write-Verbose "Installing $ToolName version $ToolVersion" - Write-Verbose "Executing '$InstallerPath $LocalInstallerArguments'" + Write-Verbose "Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({"-$_ '$($LocalInstallerArguments.$_)'"}) -join ' ')'" & $InstallerPath @LocalInstallerArguments if ($LASTEXITCODE -Ne "0") { $errMsg = "$ToolName installation failed" if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { - Write-Warning $errMsg + $showNativeToolsWarning = $true + if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) { + $showNativeToolsWarning = $false + } + if ($showNativeToolsWarning) { + Write-Warning $errMsg + } $toolInstallationFailure = $true } else { Write-Error $errMsg diff --git a/eng/common/init-tools-native.sh b/eng/common/init-tools-native.sh index 5f2e77f448..4dafaaca13 100755 --- a/eng/common/init-tools-native.sh +++ b/eng/common/init-tools-native.sh @@ -70,8 +70,7 @@ function ReadGlobalJsonNativeTools { # Only extract the contents of the object. local native_tools_list=$(echo $native_tools_section | awk -F"[{}]" '{print $2}') native_tools_list=${native_tools_list//[\" ]/} - native_tools_list=${native_tools_list//,/$'\n'} - native_tools_list="$(echo -e "${native_tools_list}" | tr -d '[[:space:]]')" + native_tools_list=$( echo "$native_tools_list" | sed 's/\s//g' | sed 's/,/\n/g' ) local old_IFS=$IFS while read -r line; do @@ -108,6 +107,7 @@ else installer_command+=" --baseuri $base_uri" installer_command+=" --installpath $install_bin" installer_command+=" --version $tool_version" + echo $installer_command if [[ $force = true ]]; then installer_command+=" --force" diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh old mode 100644 new mode 100755 diff --git a/eng/common/native/CommonLibrary.psm1 b/eng/common/native/CommonLibrary.psm1 index 7a34c7e8a4..2a08d5246e 100644 --- a/eng/common/native/CommonLibrary.psm1 +++ b/eng/common/native/CommonLibrary.psm1 @@ -59,9 +59,38 @@ function DownloadAndExtract { -Verbose:$Verbose if ($UnzipStatus -Eq $False) { - Write-Error "Unzip failed" - return $False + # Retry Download one more time with Force=true + $DownloadRetryStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries 1 ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$True ` + -Verbose:$Verbose + + if ($DownloadRetryStatus -Eq $False) { + Write-Error "Last attempt of download failed as well" + return $False + } + + # Retry unzip again one more time with Force=true + $UnzipRetryStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$True ` + -Verbose:$Verbose + if ($UnzipRetryStatus -Eq $False) + { + Write-Error "Last attempt of unzip failed as well" + # Clean up partial zips and extracts + if (Test-Path $TempToolPath) { + Remove-Item $TempToolPath -Force + } + if (Test-Path $InstallDirectory) { + Remove-Item $InstallDirectory -Force -Recurse + } + return $False + } } + return $True } diff --git a/eng/common/native/install-cmake-test.sh b/eng/common/native/install-cmake-test.sh new file mode 100755 index 0000000000..53ddf4e686 --- /dev/null +++ b/eng/common/native/install-cmake-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake-test" +tool_os=$(GetCurrentOS) +tool_folder=$(echo $tool_os | awk '{print tolower($0)}') +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + echo "Installation failed" >&2 + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + echo "Shim generation failed" >&2 + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/eng/common/native/install-cmake.sh b/eng/common/native/install-cmake.sh index 293af6017d..5f1a182fa9 100755 --- a/eng/common/native/install-cmake.sh +++ b/eng/common/native/install-cmake.sh @@ -69,7 +69,7 @@ tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" tool_install_directory="$install_path/$tool_name/$version" tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" shim_path="$install_path/$tool_name.sh" -uri="${base_uri}/$tool_folder/cmake/$tool_name_moniker.tar.gz" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" # Clean up tool and installers if [[ $clean = true ]]; then diff --git a/eng/common/performance/performance-setup.sh b/eng/common/performance/performance-setup.sh old mode 100644 new mode 100755 diff --git a/eng/common/pipeline-logging-functions.sh b/eng/common/pipeline-logging-functions.sh old mode 100644 new mode 100755 diff --git a/eng/common/post-build/darc-gather-drop.ps1 b/eng/common/post-build/darc-gather-drop.ps1 new file mode 100644 index 0000000000..93a0bd8328 --- /dev/null +++ b/eng/common/post-build/darc-gather-drop.ps1 @@ -0,0 +1,35 @@ +param( + [Parameter(Mandatory=$true)][int] $BarBuildId, # ID of the build which assets should be downloaded + [Parameter(Mandatory=$true)][string] $DropLocation, # Where the assets should be downloaded to + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, # Token used to access Maestro API + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", # Maestro API URL + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" # Version of Maestro API to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + Write-Host "Installing DARC ..." + + . $PSScriptRoot\..\darc-init.ps1 + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + Write-PipelineTaskError "Something failed while running 'darc-init.ps1'. Check for errors above. Exiting now..." + ExitWithExitCode $exitCode + } + + darc gather-drop --non-shipping ` + --continue-on-error ` + --id $BarBuildId ` + --output-dir $DropLocation ` + --bar-uri $MaestroApiEndpoint ` + --password $MaestroApiAccessToken ` + --latest-location +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/nuget-validation.ps1 b/eng/common/post-build/nuget-validation.ps1 index 1bdced1e30..78ed0d540f 100644 --- a/eng/common/post-build/nuget-validation.ps1 +++ b/eng/common/post-build/nuget-validation.ps1 @@ -6,10 +6,7 @@ param( [Parameter(Mandatory=$true)][string] $ToolDestinationPath # Where the validation tool should be downloaded to ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 try { $url = "https://raw.githubusercontent.com/NuGet/NuGetGallery/jver-verify/src/VerifyMicrosoftPackage/verify.ps1" diff --git a/eng/common/post-build/post-build-utils.ps1 b/eng/common/post-build/post-build-utils.ps1 new file mode 100644 index 0000000000..551ae113f8 --- /dev/null +++ b/eng/common/post-build/post-build-utils.ps1 @@ -0,0 +1,90 @@ +# Most of the functions in this file require the variables `MaestroApiEndPoint`, +# `MaestroApiVersion` and `MaestroApiAccessToken` to be globally available. + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +# `tools.ps1` checks $ci to perform some actions. Since the post-build +# scripts don't necessarily execute in the same agent that run the +# build.ps1/sh script this variable isn't automatically set. +$ci = $true +. $PSScriptRoot\..\tools.ps1 + +function Create-MaestroApiRequestHeaders([string]$ContentType = "application/json") { + Validate-MaestroVars + + $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $headers.Add('Accept', $ContentType) + $headers.Add('Authorization',"Bearer $MaestroApiAccessToken") + return $headers +} + +function Get-MaestroChannel([int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}?api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroBuild([int]$BuildId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/builds/${BuildId}?api-version=$MaestroApiVersion" + + $result = try { return Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroSubscriptions([string]$SourceRepository, [int]$ChannelId) { + Validate-MaestroVars + + $SourceRepository = [System.Web.HttpUtility]::UrlEncode($SourceRepository) + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions?sourceRepository=$SourceRepository&channelId=$ChannelId&api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Trigger-Subscription([string]$SubscriptionId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions/$SubscriptionId/trigger?api-version=$MaestroApiVersion" + Invoke-WebRequest -Uri $apiEndpoint -Headers $apiHeaders -Method Post | Out-Null +} + +function Assign-BuildToChannel([int]$BuildId, [int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$MaestroApiVersion" + Invoke-WebRequest -Method Post -Uri $apiEndpoint -Headers $apiHeaders | Out-Null +} + +function Validate-MaestroVars { + try { + Get-Variable MaestroApiEndPoint -Scope Global | Out-Null + Get-Variable MaestroApiVersion -Scope Global | Out-Null + Get-Variable MaestroApiAccessToken -Scope Global | Out-Null + + if (!($MaestroApiEndPoint -Match "^http[s]?://maestro-(int|prod).westus2.cloudapp.azure.com$")) { + Write-PipelineTaskError "MaestroApiEndPoint is not a valid Maestro URL. '$MaestroApiEndPoint'" + ExitWithExitCode 1 + } + + if (!($MaestroApiVersion -Match "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) { + Write-PipelineTaskError "MaestroApiVersion does not match a version string in the format yyyy-MM-DD. '$MaestroApiVersion'" + ExitWithExitCode 1 + } + } + catch { + Write-PipelineTaskError "Error: Variables `MaestroApiEndPoint`, `MaestroApiVersion` and `MaestroApiAccessToken` are required while using this script." + Write-Host $_ + ExitWithExitCode 1 + } +} diff --git a/eng/common/post-build/promote-build.ps1 b/eng/common/post-build/promote-build.ps1 index 84a608fa56..e5ae85f251 100644 --- a/eng/common/post-build/promote-build.ps1 +++ b/eng/common/post-build/promote-build.ps1 @@ -1,30 +1,25 @@ param( [Parameter(Mandatory=$true)][int] $BuildId, [Parameter(Mandatory=$true)][int] $ChannelId, - [Parameter(Mandatory=$true)][string] $BarToken, - [string] $MaestroEndpoint = "https://maestro-prod.westus2.cloudapp.azure.com", - [string] $ApiVersion = "2019-01-16" + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 - -function Get-Headers([string]$accept, [string]$barToken) { - $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' - $headers.Add('Accept',$accept) - $headers.Add('Authorization',"Bearer $barToken") - return $headers -} +. $PSScriptRoot\post-build-utils.ps1 try { - $maestroHeaders = Get-Headers 'application/json' $BarToken + # Check that the channel we are going to promote the build to exist + $channelInfo = Get-MaestroChannel -ChannelId $ChannelId + + if (!$channelInfo) { + Write-Host "Channel with BAR ID $ChannelId was not found in BAR!" + ExitWithExitCode 1 + } # Get info about which channels the build has already been promoted to - $getBuildApiEndpoint = "$MaestroEndpoint/api/builds/${BuildId}?api-version=$ApiVersion" - $buildInfo = Invoke-WebRequest -Method Get -Uri $getBuildApiEndpoint -Headers $maestroHeaders | ConvertFrom-Json - + $buildInfo = Get-MaestroBuild -BuildId $BuildId + if (!$buildInfo) { Write-Host "Build with BAR ID $BuildId was not found in BAR!" ExitWithExitCode 1 @@ -40,10 +35,10 @@ try { } } - Write-Host "Build not present in channel $ChannelId. Promoting build ... " + Write-Host "Promoting build '$BuildId' to channel '$ChannelId'." + + Assign-BuildToChannel -BuildId $BuildId -ChannelId $ChannelId - $promoteBuildApiEndpoint = "$maestroEndpoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$ApiVersion" - Invoke-WebRequest -Method Post -Uri $promoteBuildApiEndpoint -Headers $maestroHeaders Write-Host "done." } catch { diff --git a/eng/common/post-build/setup-maestro-vars.ps1 b/eng/common/post-build/setup-maestro-vars.ps1 new file mode 100644 index 0000000000..d7f64dc63c --- /dev/null +++ b/eng/common/post-build/setup-maestro-vars.ps1 @@ -0,0 +1,26 @@ +param( + [Parameter(Mandatory=$true)][string] $ReleaseConfigsPath # Full path to ReleaseConfigs.txt asset +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + $Content = Get-Content $ReleaseConfigsPath + + $BarId = $Content | Select -Index 0 + + $Channels = "" + $Content | Select -Index 1 | ForEach-Object { $Channels += "$_ ," } + + $IsStableBuild = $Content | Select -Index 2 + + Write-PipelineSetVariable -Name 'BARBuildId' -Value $BarId + Write-PipelineSetVariable -Name 'InitialChannels' -Value "$Channels" + Write-PipelineSetVariable -Name 'IsStableBuild' -Value $IsStableBuild +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 index 8abd684e9e..41e01ae6e6 100644 --- a/eng/common/post-build/sourcelink-validation.ps1 +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -6,10 +6,7 @@ param( [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion # Version of SourceLink CLI to use ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 # Cache/HashMap (File -> Exist flag) used to consult whether a file exist # in the repository at a specific commit point. This is populated by inserting @@ -200,21 +197,27 @@ function ValidateSourceLinkLinks { } } -function CheckExitCode ([string]$stage) { - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-PipelineTaskError "Something failed while '$stage'. Check for errors above. Exiting now..." - ExitWithExitCode $exitCode +function InstallSourcelinkCli { + $sourcelinkCliPackageName = "sourcelink" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { + Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." + } + else { + Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global } } try { - Write-Host "Installing SourceLink CLI..." - Get-Location - . $PSScriptRoot\sourcelink-cli-init.ps1 -sourcelinkCliVersion $SourcelinkCliVersion - CheckExitCode "Running sourcelink-cli-init" + InstallSourcelinkCli - Measure-Command { ValidateSourceLinkLinks } + ValidateSourceLinkLinks } catch { Write-Host $_ diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 index 69456854e0..d5ec51b150 100644 --- a/eng/common/post-build/symbols-validation.ps1 +++ b/eng/common/post-build/symbols-validation.ps1 @@ -4,10 +4,7 @@ param( [Parameter(Mandatory=$true)][string] $DotnetSymbolVersion # Version of dotnet symbol to use ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 Add-Type -AssemblyName System.IO.Compression.FileSystem @@ -162,19 +159,25 @@ function CheckSymbolsAvailable { } } -function CheckExitCode ([string]$stage) { - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-PipelineTaskError "Something failed while '$stage'. Check for errors above. Exiting now..." - ExitWithExitCode $exitCode +function Installdotnetsymbol { + $dotnetsymbolPackageName = "dotnet-symbol" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$dotnetsymbolPackageName*") -and ($toolList -like "*$dotnetsymbolVersion*")) { + Write-Host "dotnet-symbol version $dotnetsymbolVersion is already installed." + } + else { + Write-Host "Installing dotnet-symbol version $dotnetsymbolVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $dotnetsymbolPackageName --version $dotnetsymbolVersion --verbosity "minimal" --global } } try { - Write-Host "Installing dotnet symbol ..." - Get-Location - . $PSScriptRoot\dotnetsymbol-init.ps1 -dotnetsymbolVersion $DotnetSymbolVersion - CheckExitCode "Running dotnetsymbol-init" + Installdotnetsymbol CheckSymbolsAvailable } diff --git a/eng/common/post-build/trigger-subscriptions.ps1 b/eng/common/post-build/trigger-subscriptions.ps1 index 1a91dab037..926d5b4551 100644 --- a/eng/common/post-build/trigger-subscriptions.ps1 +++ b/eng/common/post-build/trigger-subscriptions.ps1 @@ -1,33 +1,20 @@ -param( +param( [Parameter(Mandatory=$true)][string] $SourceRepo, [Parameter(Mandatory=$true)][int] $ChannelId, - [string] $MaestroEndpoint = "https://maestro-prod.westus2.cloudapp.azure.com", - [string] $BarToken, - [string] $ApiVersion = "2019-01-16" + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 - -function Get-Headers([string]$accept, [string]$barToken) { - $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' - $headers.Add('Accept',$accept) - $headers.Add('Authorization',"Bearer $barToken") - return $headers -} +. $PSScriptRoot\post-build-utils.ps1 # Get all the $SourceRepo subscriptions $normalizedSourceRepo = $SourceRepo.Replace('dnceng@', '') -$getSubscriptionsApiEndpoint = "$maestroEndpoint/api/subscriptions?sourceRepository=$normalizedSourceRepo&api-version=$apiVersion" -$headers = Get-Headers 'application/json' $barToken - -$subscriptions = Invoke-WebRequest -Uri $getSubscriptionsApiEndpoint -Headers $headers | ConvertFrom-Json +$subscriptions = Get-MaestroSubscriptions -SourceRepository $normalizedSourceRepo -ChannelId $ChannelId if (!$subscriptions) { Write-Host "No subscriptions found for source repo '$normalizedSourceRepo' in channel '$ChannelId'" - return + ExitWithExitCode 0 } $subscriptionsToTrigger = New-Object System.Collections.Generic.List[string] @@ -36,21 +23,18 @@ $failedTriggeredSubscription = $false # Get all enabled subscriptions that need dependency flow on 'everyBuild' foreach ($subscription in $subscriptions) { if ($subscription.enabled -and $subscription.policy.updateFrequency -like 'everyBuild' -and $subscription.channel.id -eq $ChannelId) { - Write-Host "$subscription.id" + Write-Host "Should trigger this subscription: $subscription.id" [void]$subscriptionsToTrigger.Add($subscription.id) } } foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { try { - $triggerSubscriptionApiEndpoint = "$maestroEndpoint/api/subscriptions/$subscriptionToTrigger/trigger?api-version=$apiVersion" - $headers = Get-Headers 'application/json' $BarToken - - Write-Host "Triggering subscription '$subscriptionToTrigger'..." + Write-Host "Triggering subscription '$subscriptionToTrigger'." - Invoke-WebRequest -Uri $triggerSubscriptionApiEndpoint -Headers $headers -Method Post + Trigger-Subscription -SubscriptionId $subscriptionToTrigger - Write-Host "Subscription '$subscriptionToTrigger' triggered!" + Write-Host "done." } catch { @@ -61,9 +45,13 @@ foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { } } -if ($failedTriggeredSubscription) { +if ($subscriptionsToTrigger.Count -eq 0) { + Write-Host "No subscription matched source repo '$normalizedSourceRepo' and channel ID '$ChannelId'." +} +elseif ($failedTriggeredSubscription) { Write-Host "At least one subscription failed to be triggered..." ExitWithExitCode 1 } - -Write-Host "All subscriptions were triggered successfully!" +else { + Write-Host "All subscriptions were triggered successfully!" +} diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config index fb9b712863..3f97ac2f16 100644 --- a/eng/common/sdl/packages.config +++ b/eng/common/sdl/packages.config @@ -1,4 +1,4 @@  - + diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml index 5837f3d56d..f657a4dc91 100644 --- a/eng/common/templates/job/execute-sdl.yml +++ b/eng/common/templates/job/execute-sdl.yml @@ -46,7 +46,7 @@ jobs: continueOnError: ${{ parameters.continueOnError }} - ${{ if eq(parameters.overrideParameters, '') }}: - powershell: eng/common/sdl/execute-all-sdl-tools.ps1 - -GuardianPackageName Microsoft.Guardian.Cli.0.6.0 + -GuardianPackageName Microsoft.Guardian.Cli.0.7.1 -NugetPackageDirectory $(Build.SourcesDirectory)\.packages -AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw) ${{ parameters.additionalParameters }} diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index 1814e0ab61..8db456bb7f 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -37,6 +37,9 @@ parameters: # Optional: Enable publishing to the build asset registry enablePublishBuildAssets: false + # Optional: Prevent gather/push manifest from executing when using publishing pipelines + enablePublishUsingPipelines: false + # Optional: Include PublishTestResults task enablePublishTestResults: false @@ -187,7 +190,7 @@ jobs: continueOnError: true condition: always() - - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(variables['_PublishUsingPipelines'], 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(parameters.enablePublishUsingPipelines, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - task: CopyFiles@2 displayName: Gather Asset Manifests inputs: @@ -195,6 +198,7 @@ jobs: TargetFolder: '$(Build.StagingDirectory)/AssetManifests' continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + - task: PublishBuildArtifacts@1 displayName: Push Asset Manifests inputs: diff --git a/eng/common/templates/post-build/channels/internal-servicing.yml b/eng/common/templates/post-build/channels/internal-servicing.yml index 648e854e0e..dc065ab308 100644 --- a/eng/common/templates/post-build/channels/internal-servicing.yml +++ b/eng/common/templates/post-build/channels/internal-servicing.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -87,8 +86,8 @@ stages: /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' @@ -127,7 +126,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -143,29 +142,6 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location - enabled: false - - template: ../promote-build.yml parameters: ChannelId: ${{ variables.InternalServicing_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-dev-5.yml b/eng/common/templates/post-build/channels/netcore-dev-5.yml new file mode 100644 index 0000000000..f2b0cfb269 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-dev-5.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Dev5_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Core 5 Dev Channel + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_5_Dev_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Dev5_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-tools-latest.yml b/eng/common/templates/post-build/channels/netcore-tools-latest.yml new file mode 100644 index 0000000000..fd6c09b227 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-tools-latest.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Tools_Latest_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Tools - Latest + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_Tools_Latest_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Tools_Latest_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-dev-release.yml b/eng/common/templates/post-build/channels/public-dev-release.yml index bdc631016b..771dcf4ef8 100644 --- a/eng/common/templates/post-build/channels/public-dev-release.yml +++ b/eng/common/templates/post-build/channels/public-dev-release.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -77,7 +76,7 @@ stages: filePath: eng\common\sdk-task.ps1 arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet /p:ChannelId=$(PublicDevRelease_30_Channel_Id) - /p:ArtifactsCategory=.NetCore + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) /p:IsStableBuild=$(IsStableBuild) /p:IsInternalBuild=$(IsInternalBuild) /p:RepositoryName=$(Build.Repository.Name) @@ -87,8 +86,8 @@ stages: /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' @@ -124,7 +123,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -140,27 +139,9 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicDevRelease_30_Channel_Id }} - template: ../promote-build.yml parameters: diff --git a/eng/common/templates/post-build/channels/public-release.yml b/eng/common/templates/post-build/channels/public-release.yml index f6a7efdfe9..00108bd3f8 100644 --- a/eng/common/templates/post-build/channels/public-release.yml +++ b/eng/common/templates/post-build/channels/public-release.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -87,8 +86,8 @@ stages: /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' @@ -127,7 +126,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -143,29 +142,6 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location - enabled: false - - template: ../promote-build.yml parameters: ChannelId: ${{ variables.PublicRelease_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-validation-release.yml b/eng/common/templates/post-build/channels/public-validation-release.yml index f12f402ad9..f64184da9f 100644 --- a/eng/common/templates/post-build/channels/public-validation-release.yml +++ b/eng/common/templates/post-build/channels/public-validation-release.yml @@ -12,13 +12,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicValidationRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicValidationRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -48,7 +47,7 @@ stages: filePath: eng\common\sdk-task.ps1 arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet /p:ChannelId=$(PublicValidationRelease_30_Channel_Id) - /p:ArtifactsCategory=.NetCoreValidation + /p:ArtifactsCategory=$(_DotNetValidationArtifactsCategory) /p:IsStableBuild=$(IsStableBuild) /p:IsInternalBuild=$(IsInternalBuild) /p:RepositoryName=$(Build.Repository.Name) @@ -58,13 +57,13 @@ stages: /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' /p:Configuration=Release - + - task: NuGetCommand@2 displayName: Publish Packages to AzDO Feed condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') @@ -91,29 +90,9 @@ stages: jobs: - template: ../setup-maestro-vars.yml - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicValidationRelease_30_Channel_Id) - variables: - - name: BARBuildId - value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - - group: Publish-Build-Assets - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com --password $(MaestroAccessToken) --latest-location + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicValidationRelease_30_Channel_Id }} - template: ../promote-build.yml parameters: diff --git a/eng/common/templates/post-build/common-variables.yml b/eng/common/templates/post-build/common-variables.yml index 42df4ae77e..52a74487fd 100644 --- a/eng/common/templates/post-build/common-variables.yml +++ b/eng/common/templates/post-build/common-variables.yml @@ -1,21 +1,47 @@ variables: + - group: Publish-Build-Assets + # .NET Core 3 Dev - PublicDevRelease_30_Channel_Id: 3 + - name: PublicDevRelease_30_Channel_Id + value: 3 + + # .NET Core 5 Dev + - name: NetCore_5_Dev_Channel_Id + value: 131 # .NET Tools - Validation - PublicValidationRelease_30_Channel_Id: 9 + - name: PublicValidationRelease_30_Channel_Id + value: 9 + + # .NET Tools - Latest + - name: NetCore_Tools_Latest_Channel_Id + value: 2 # .NET Core 3.0 Internal Servicing - InternalServicing_30_Channel_Id: 184 + - name: InternalServicing_30_Channel_Id + value: 184 # .NET Core 3.0 Release - PublicRelease_30_Channel_Id: 19 + - name: PublicRelease_30_Channel_Id + value: 19 # Whether the build is internal or not - IsInternalBuild: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} + - name: IsInternalBuild + value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} # Storage account name for proxy-backed feeds - ProxyBackedFeedsAccountName: dotnetfeed + - name: ProxyBackedFeedsAccountName + value: dotnetfeed - SourceLinkCLIVersion: 3.0.0 - SymbolToolVersion: 1.0.1 + # Default Maestro++ API Endpoint and API Version + - name: MaestroApiEndPoint + value: "https://maestro-prod.westus2.cloudapp.azure.com" + - name: MaestroApiAccessToken + value: $(MaestroAccessToken) + - name: MaestroApiVersion + value: "2019-01-16" + + - name: SourceLinkCLIVersion + value: 3.0.0 + - name: SymbolToolVersion + value: 1.0.1 diff --git a/eng/common/templates/post-build/darc-gather-drop.yml b/eng/common/templates/post-build/darc-gather-drop.yml new file mode 100644 index 0000000000..3268ccaa55 --- /dev/null +++ b/eng/common/templates/post-build/darc-gather-drop.yml @@ -0,0 +1,23 @@ +parameters: + ChannelId: 0 + +jobs: +- job: gatherDrop + displayName: Gather Drop + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) + variables: + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + pool: + vmImage: 'windows-2019' + steps: + - task: PowerShell@2 + displayName: Darc gather-drop + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/darc-gather-drop.ps1 + arguments: -BarBuildId $(BARBuildId) + -DropLocation $(Agent.BuildDirectory)/Temp/Drop/ + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index daa799259c..33db50ce26 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -7,9 +7,12 @@ parameters: enable: false params: '' + # Which stages should finish execution before post-build stages start + dependsOn: [build] + stages: - stage: validate - dependsOn: build + dependsOn: ${{ parameters.dependsOn }} displayName: Validate jobs: - ${{ if eq(parameters.enableNugetValidation, 'true') }}: @@ -80,10 +83,18 @@ stages: parameters: additionalParameters: ${{ parameters.SDLValidationParameters.params }} +- template: \eng\common\templates\post-build\channels\netcore-dev-5.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + - template: \eng\common\templates\post-build\channels\public-dev-release.yml parameters: enableSymbolValidation: ${{ parameters.enableSymbolValidation }} +- template: \eng\common\templates\post-build\channels\netcore-tools-latest.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + - template: \eng\common\templates\post-build\channels\public-validation-release.yml - template: \eng\common\templates\post-build\channels\public-release.yml diff --git a/eng/common/templates/post-build/promote-build.yml b/eng/common/templates/post-build/promote-build.yml index af48b0b339..6b479c3b82 100644 --- a/eng/common/templates/post-build/promote-build.yml +++ b/eng/common/templates/post-build/promote-build.yml @@ -5,13 +5,12 @@ jobs: - job: displayName: Promote Build dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], ${{ parameters.ChannelId }}) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) variables: - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: ChannelId value: ${{ parameters.ChannelId }} - - group: Publish-Build-Assets pool: vmImage: 'windows-2019' steps: @@ -21,4 +20,6 @@ jobs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/promote-build.ps1 arguments: -BuildId $(BARBuildId) -ChannelId $(ChannelId) - -BarToken $(MaestroAccessToken) + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/setup-maestro-vars.yml b/eng/common/templates/post-build/setup-maestro-vars.yml index f6120dc1e1..56242b068e 100644 --- a/eng/common/templates/post-build/setup-maestro-vars.yml +++ b/eng/common/templates/post-build/setup-maestro-vars.yml @@ -14,22 +14,5 @@ jobs: name: setReleaseVars displayName: Set Release Configs Vars inputs: - targetType: inline - script: | - # This is needed to make Write-PipelineSetVariable works in this context - $ci = $true - - . "$(Build.SourcesDirectory)/eng/common/tools.ps1" - - $Content = Get-Content "$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt" - - $BarId = $Content | Select -Index 0 - - $Channels = "" - $Content | Select -Index 1 | ForEach-Object { $Channels += "$_ ," } - - $IsStableBuild = $Content | Select -Index 2 - - Write-PipelineSetVariable -Name 'BARBuildId' -Value $BarId - Write-PipelineSetVariable -Name 'InitialChannels' -Value "$Channels" - Write-PipelineSetVariable -Name 'IsStableBuild' -Value $IsStableBuild + filePath: $(Build.SourcesDirectory)/eng/common/post-build/setup-maestro-vars.ps1 + arguments: -ReleaseConfigsPath '$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt' diff --git a/eng/common/templates/post-build/trigger-subscription.yml b/eng/common/templates/post-build/trigger-subscription.yml index 65259d4e68..da669030da 100644 --- a/eng/common/templates/post-build/trigger-subscription.yml +++ b/eng/common/templates/post-build/trigger-subscription.yml @@ -8,4 +8,6 @@ steps: filePath: $(Build.SourcesDirectory)/eng/common/post-build/trigger-subscriptions.ps1 arguments: -SourceRepo $(Build.Repository.Uri) -ChannelId ${{ parameters.ChannelId }} - -BarToken $(MaestroAccessTokenInt) \ No newline at end of file + -MaestroApiAccessToken $(MaestroAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 8fe2b11ad2..9c12b1b4fd 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -39,6 +39,10 @@ # installed on the machine instead of downloading one. [bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true } +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dot.net/v1/dotnet-install.ps1 +[string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { "v1" } + # True to use global NuGet cache instead of restoring packages to repository-local directory. [bool]$useGlobalNuGetCache = if (Test-Path variable:useGlobalNuGetCache) { $useGlobalNuGetCache } else { !$ci } @@ -159,7 +163,7 @@ function GetDotNetInstallScript([string] $dotnetRoot) { $installScript = Join-Path $dotnetRoot "dotnet-install.ps1" if (!(Test-Path $installScript)) { Create-Directory $dotnetRoot - Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript + Invoke-WebRequest "https://dot.net/$dotnetInstallScriptVersion/dotnet-install.ps1" -OutFile $installScript } return $installScript @@ -518,6 +522,9 @@ function MSBuild-Core() { if ($warnAsError) { $cmdArgs += " /warnaserror /p:TreatWarningsAsErrors=true" } + else { + $cmdArgs += " /p:TreatWarningsAsErrors=false" + } foreach ($arg in $args) { if ($arg -ne $null -and $arg.Trim() -ne "") { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 0deb01c480..3af9be6157 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -45,6 +45,10 @@ warn_as_error=${warn_as_error:-true} # installed on the machine instead of downloading one. use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dot.net/v1/dotnet-install.sh +dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} + # True to use global NuGet cache instead of restoring packages to repository-local directory. if [[ "$ci" == true ]]; then use_global_nuget_cache=${use_global_nuget_cache:-false} @@ -77,7 +81,7 @@ function ReadGlobalVersion { local pattern="\"$key\" *: *\"(.*)\"" if [[ ! $line =~ $pattern ]]; then - Write-PipelineTelemetryError -category 'InitializeTools' "Error: Cannot find \"$key\" in $global_json_file" + Write-PipelineTelemetryError -category 'InitializeToolset' "Error: Cannot find \"$key\" in $global_json_file" ExitWithExitCode 1 fi @@ -195,7 +199,7 @@ function InstallDotNet { function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" - local install_script_url="https://dot.net/v1/dotnet-install.sh" + local install_script_url="https://dot.net/$dotnetInstallScriptVersion/dotnet-install.sh" if [[ ! -a "$install_script" ]]; then mkdir -p "$root" @@ -245,7 +249,7 @@ function InitializeNativeTools() { then local nativeArgs="" if [[ "$ci" == true ]]; then - nativeArgs="-InstallDirectory $tools_dir" + nativeArgs="--installDirectory $tools_dir" fi "$_script_dir/init-tools-native.sh" $nativeArgs fi diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1 index 669b56c21f..5ed823f083 100644 --- a/eng/scripts/CodeCheck.ps1 +++ b/eng/scripts/CodeCheck.ps1 @@ -166,11 +166,6 @@ try { & dotnet run -p "$repoRoot/eng/tools/BaselineGenerator/" } - Write-Host "Re-generating Web.JS files" - Invoke-Block { - & dotnet build "$repoRoot\src\Components\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" - } - Write-Host "Run git diff to check for pending changes" # Redirect stderr to stdout because PowerShell does not consistently handle output to stderr diff --git a/eng/targets/Npm.Common.targets b/eng/targets/Npm.Common.targets index c290e39756..204e14d01f 100644 --- a/eng/targets/Npm.Common.targets +++ b/eng/targets/Npm.Common.targets @@ -11,15 +11,28 @@ $([MSBuild]::NormalizeDirectory('$(BaseIntermediateOutputPath)'))$(Configuration)\ --frozen-lockfile <_BackupPackageJson>$(IntermediateOutputPath)$(MSBuildProjectName).package.json.bak + + PrepareForBuild; + ResolveProjectReferences; + _Build; + + run build + + + + + + + @@ -36,13 +49,13 @@ BuildInParallel="true" /> - + - + diff --git a/global.json b/global.json index 4f83cb7a89..64afa6336e 100644 --- a/global.json +++ b/global.json @@ -24,7 +24,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.15.2", - "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19369.2", - "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19369.2" + "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19404.1", + "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19404.1" } } diff --git a/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs b/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs new file mode 100644 index 0000000000..3f0c765614 --- /dev/null +++ b/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs @@ -0,0 +1,39 @@ +// 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.Immutable; +using Microsoft.AspNetCore.Components.Analyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.Internal +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ComponentInternalUsageDiagnosticAnalyzer : DiagnosticAnalyzer + { + private readonly InternalUsageAnalyzer _inner; + + public ComponentInternalUsageDiagnosticAnalyzer() + { + // We don't have in *internal* attribute in Blazor. + _inner = new InternalUsageAnalyzer(IsInInternalNamespace, hasInternalAttribute: null, DiagnosticDescriptors.DoNotUseRenderTreeTypes); + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotUseRenderTreeTypes); + + public override void Initialize(AnalysisContext context) + { + _inner.Register(context); + } + + private static bool IsInInternalNamespace(ISymbol symbol) + { + if (symbol?.ContainingNamespace?.ToDisplayString() is string ns) + { + return string.Equals(ns, "Microsoft.AspNetCore.Components.RenderTree"); + } + + return false; + } + } +} diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs index e0ecbce055..8eb86b3212 100644 --- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs +++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs @@ -55,5 +55,14 @@ namespace Microsoft.AspNetCore.Components.Analyzers DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Description), Resources.ResourceManager, typeof(Resources))); + + public static readonly DiagnosticDescriptor DoNotUseRenderTreeTypes = new DiagnosticDescriptor( + "BL0006", + new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources))); } } diff --git a/src/Components/Analyzers/src/InternalUsageAnalyzer.cs b/src/Components/Analyzers/src/InternalUsageAnalyzer.cs new file mode 100644 index 0000000000..92b07a7ab2 --- /dev/null +++ b/src/Components/Analyzers/src/InternalUsageAnalyzer.cs @@ -0,0 +1,126 @@ +// 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; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.Internal +{ + internal class InternalUsageAnalyzer + { + private readonly Func _isInternalNamespace; + private readonly Func _hasInternalAttribute; + private readonly DiagnosticDescriptor _descriptor; + + /// + /// Creates a new instance of . The creator should provide delegates to help determine whether + /// a given symbol is internal or not, and a to create errors. + /// + /// The delegate used to check if a symbol belongs to an internal namespace. + /// The delegate used to check if a symbol has an internal attribute. + /// + /// The used to create errors. The error message should expect a single parameter + /// used for the display name of the member. + /// + public InternalUsageAnalyzer(Func isInInternalNamespace, Func hasInternalAttribute, DiagnosticDescriptor descriptor) + { + _isInternalNamespace = isInInternalNamespace ?? new Func((_) => false); + _hasInternalAttribute = hasInternalAttribute ?? new Func((_) => false); + _descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + } + + public void Register(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, + SyntaxKind.SimpleMemberAccessExpression, + SyntaxKind.ObjectCreationExpression, + SyntaxKind.ClassDeclaration, + SyntaxKind.Parameter); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + switch (context.Node) + { + case MemberAccessExpressionSyntax memberAccessSyntax: + { + if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly) + { + var containingType = symbol.ContainingType; + + if (HasInternalAttribute(symbol)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), $"{containingType}.{symbol.Name}")); + return; + } + + if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), containingType)); + return; + } + } + return; + } + + case ObjectCreationExpressionSyntax creationSyntax: + { + if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly) + { + var containingType = symbol.ContainingType; + + if (HasInternalAttribute(symbol)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.GetLocation(), containingType)); + return; + } + + if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.Type.GetLocation(), containingType)); + return; + } + } + + return; + } + + case ClassDeclarationSyntax declarationSyntax: + { + if (context.SemanticModel.GetDeclaredSymbol(declarationSyntax)?.BaseType is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly && + (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol)) && + declarationSyntax.BaseList?.Types.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, declarationSyntax.BaseList.Types[0].GetLocation(), symbol)); + } + + return; + } + + case ParameterSyntax parameterSyntax: + { + if (context.SemanticModel.GetDeclaredSymbol(parameterSyntax)?.Type is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly && + (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol))) + { + + context.ReportDiagnostic(Diagnostic.Create(_descriptor, parameterSyntax.GetLocation(), symbol)); + } + + return; + } + } + } + + private bool HasInternalAttribute(ISymbol symbol) => _hasInternalAttribute(symbol); + + private bool IsInInternalNamespace(ISymbol symbol) => _isInternalNamespace(symbol); + } +} diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx index 648adb2c3b..ed4f36c531 100644 --- a/src/Components/Analyzers/src/Resources.resx +++ b/src/Components/Analyzers/src/Resources.resx @@ -1,17 +1,17 @@ - @@ -165,4 +165,13 @@ Component parameter should not be set outside of its component. + + The types in 'Microsoft.AspNetCore.Components.RenderTree' are not recommended for use outside of the Blazor framework. These type definitions will change in future releases. + + + The type or member {0} is is not recommended for use outside of the Blazor frameworks. Types defined in 'Microsoft.AspNetCore.Components.RenderTree' will change in future releases. + + + Do not use RenderTree types + \ No newline at end of file diff --git a/src/Components/Analyzers/test/AnalyzerTestBase.cs b/src/Components/Analyzers/test/AnalyzerTestBase.cs new file mode 100644 index 0000000000..e174b4f667 --- /dev/null +++ b/src/Components/Analyzers/test/AnalyzerTestBase.cs @@ -0,0 +1,68 @@ +// 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; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + public abstract class AnalyzerTestBase + { + private static readonly string ProjectDirectory = GetProjectDirectory(); + + public TestSource Read(string source) + { + if (!source.EndsWith(".cs")) + { + source = source + ".cs"; + } + + var filePath = Path.Combine(ProjectDirectory, "TestFiles", GetType().Name, source); + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"TestFile {source} could not be found at {filePath}.", filePath); + } + + var fileContent = File.ReadAllText(filePath); + return TestSource.Read(fileContent); + } + + public Project CreateProject(string source) + { + if (!source.EndsWith(".cs")) + { + source = source + ".cs"; + } + + var read = Read(source); + return DiagnosticProject.Create(GetType().Assembly, new[] { read.Source, }); + } + + public Task CreateCompilationAsync(string source) + { + return CreateProject(source).GetCompilationAsync(); + } + + private static string GetProjectDirectory() + { + // On helix we use the published test files + if (SkipOnHelixAttribute.OnHelix()) + { + return AppContext.BaseDirectory; + } + + // This test code needs to be updated to support distributed testing. + // See https://github.com/aspnet/AspNetCore/issues/10422 +#pragma warning disable 0618 + var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("Components"); +#pragma warning restore 0618 + var projectDirectory = Path.Combine(solutionDirectory, "Analyzers", "test"); + return projectDirectory; + } + } +} diff --git a/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs b/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs new file mode 100644 index 0000000000..727f060bd4 --- /dev/null +++ b/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs @@ -0,0 +1,31 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + internal class ComponentAnalyzerDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner + { + public ComponentAnalyzerDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer) + { + Analyzer = analyzer; + } + + public DiagnosticAnalyzer Analyzer { get; } + + public Task GetDiagnosticsAsync(string source) + { + return GetDiagnosticsAsync(sources: new[] { source }, Analyzer, Array.Empty()); + } + + public Task GetDiagnosticsAsync(Project project) + { + return GetDiagnosticsAsync(new[] { project }, Analyzer, Array.Empty()); + } + } +} diff --git a/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs b/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs new file mode 100644 index 0000000000..92e2252304 --- /dev/null +++ b/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs @@ -0,0 +1,60 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + public class ComponentInternalUsageDiagnoticsAnalyzerTest : AnalyzerTestBase + { + public ComponentInternalUsageDiagnoticsAnalyzerTest() + { + Analyzer = new ComponentInternalUsageDiagnosticAnalyzer(); + Runner = new ComponentAnalyzerDiagnosticAnalyzerRunner(Analyzer); + } + + private ComponentInternalUsageDiagnosticAnalyzer Analyzer { get; } + private ComponentAnalyzerDiagnosticAnalyzerRunner Runner { get; } + + [Fact] + public async Task InternalUsage_FindsUseOfRenderTreeFrameAsParameter() + { + // Arrange + var source = Read("UsesRenderTreeFrameAsParameter"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Collection( + diagnostics, + diagnostic => + { + Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + }); + } + + [Fact] + public async Task InternalUsage_FindsUseOfRenderTreeType() + { + // Arrange + var source = Read("UsesRenderTreeFrameTypeAsLocal"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Collection( + diagnostics, + diagnostic => + { + Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + }); + } + } +} diff --git a/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj index a31c1a8f0e..45b379c65c 100644 --- a/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj +++ b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj @@ -2,16 +2,24 @@ netcoreapp3.0 - - - - - + + + false + + + + + + + + + + diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs new file mode 100644 index 0000000000..415030a011 --- /dev/null +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +{ + class UsesRenderTreeFrameAsParameter + { + private void Test(/*MM*/RenderTreeFrame frame) + { + } + } +} diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs new file mode 100644 index 0000000000..bdd40c2df1 --- /dev/null +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +{ + class UsesRenderTreeFrameTypeAsLocal + { + private void Test() + { + var test = RenderTreeFrameType./*MM*/Attribute; + GC.KeepAlive(test); + } + + } +} diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj index ab757d5c82..adfa71ef6b 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor index c5ee6a53e2..1c360b7121 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor +++ b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor @@ -1,5 +1,10 @@ - + + + + -

Sorry, there's nothing at this address.

+ +

Sorry, there's nothing at this address.

+
diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor deleted file mode 100644 index 0f24edaf1d..0000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor index 7f4cf93fbb..fe885300e7 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor +++ b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor @@ -1 +1,8 @@ - + + + + + + Sorry, there's nothing here. + + diff --git a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj index e40ea493bd..b186c39194 100644 --- a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj +++ b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj @@ -11,7 +11,4 @@ true - - - diff --git a/src/Components/Blazor/testassets/StandaloneApp/App.razor b/src/Components/Blazor/testassets/StandaloneApp/App.razor index 4b8a0ffa42..fe6830bb01 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/App.razor +++ b/src/Components/Blazor/testassets/StandaloneApp/App.razor @@ -1,5 +1,11 @@ - - + + + + + + +

Not found

+ Sorry, there's nothing at this address. +
+
+
diff --git a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor b/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor deleted file mode 100644 index 5e11c2a20c..0000000000 --- a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index c3f77f7714..a0459a4d6f 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Components public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task task) { } } + public sealed partial class AuthorizeRouteView : Microsoft.AspNetCore.Components.RouteView + { + public AuthorizeRouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected override void Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + } public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore { public AuthorizeView() { } @@ -278,6 +287,16 @@ namespace Microsoft.AspNetCore.Components [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } + public partial class LayoutView : Microsoft.AspNetCore.Components.IComponent + { + public LayoutView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type Layout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } public sealed partial class LocationChangeException : System.Exception { public LocationChangeException(string message, System.Exception innerException) { } @@ -323,20 +342,6 @@ namespace Microsoft.AspNetCore.Components protected OwningComponentBase() { } protected TService Service { get { throw null; } } } - public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent - { - public PageDisplay() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Collections.Generic.IDictionary PageParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } - public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } - } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] public sealed partial class ParameterAttribute : System.Attribute { @@ -391,6 +396,23 @@ namespace Microsoft.AspNetCore.Components public RouteAttribute(string template) { } public string Template { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } + public sealed partial class RouteData + { + public RouteData(System.Type pageType, System.Collections.Generic.IReadOnlyDictionary routeValues) { } + public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Collections.Generic.IReadOnlyDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public partial class RouteView : Microsoft.AspNetCore.Components.IComponent + { + public RouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type DefaultLayout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RouteData RouteData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + protected virtual void Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } } namespace Microsoft.AspNetCore.Components.CompilerServices { @@ -513,13 +535,13 @@ namespace Microsoft.AspNetCore.Components.Rendering public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; } public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } } - protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { } protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; } public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } protected abstract void HandleException(System.Exception exception); protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } + protected virtual void ProcessPendingRender() { } protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; } @@ -648,9 +670,7 @@ namespace Microsoft.AspNetCore.Components.Routing [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Components.RenderFragment Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } diff --git a/src/Components/Components/src/Auth/AuthorizeRouteView.cs b/src/Components/Components/src/Auth/AuthorizeRouteView.cs new file mode 100644 index 0000000000..b0d01ab093 --- /dev/null +++ b/src/Components/Components/src/Auth/AuthorizeRouteView.cs @@ -0,0 +1,118 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Auth; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Combines the behaviors of and , + /// so that it displays the page matching the specified route but only if the user + /// is authorized to see it. + /// + /// Additionally, this component supplies a cascading parameter of type , + /// which makes the user's current authentication state available to descendants. + /// + public sealed class AuthorizeRouteView : RouteView + { + // We expect applications to supply their own authorizing/not-authorized content, but + // it's better to have defaults than to make the parameters mandatory because in some + // cases they will never be used (e.g., "authorizing" in out-of-box server-side Blazor) + private static readonly RenderFragment _defaultNotAuthorizedContent + = state => builder => builder.AddContent(0, "Not authorized"); + private static readonly RenderFragment _defaultAuthorizingContent + = builder => builder.AddContent(0, "Authorizing..."); + + private readonly RenderFragment _renderAuthorizeRouteViewCoreDelegate; + private readonly RenderFragment _renderAuthorizedDelegate; + private readonly RenderFragment _renderNotAuthorizedDelegate; + private readonly RenderFragment _renderAuthorizingDelegate; + + public AuthorizeRouteView() + { + // Cache the rendering delegates so that we only construct new closure instances + // when they are actually used (e.g., we never prepare a RenderFragment bound to + // the NotAuthorized content except when you are displaying that particular state) + RenderFragment renderBaseRouteViewDelegate = builder => base.Render(builder); + _renderAuthorizedDelegate = authenticateState => renderBaseRouteViewDelegate; + _renderNotAuthorizedDelegate = authenticationState => builder => RenderNotAuthorizedInDefaultLayout(builder, authenticationState); + _renderAuthorizingDelegate = RenderAuthorizingInDefaultLayout; + _renderAuthorizeRouteViewCoreDelegate = RenderAuthorizeRouteViewCore; + } + + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] + public RenderFragment NotAuthorized { get; set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] + public RenderFragment Authorizing { get; set; } + + [CascadingParameter] + private Task ExistingCascadedAuthenticationState { get; set; } + + /// + protected override void Render(RenderTreeBuilder builder) + { + if (ExistingCascadedAuthenticationState != null) + { + // If this component is already wrapped in a (or another + // compatible provider), then don't interfere with the cascaded authentication state. + _renderAuthorizeRouteViewCoreDelegate(builder); + } + else + { + // Otherwise, implicitly wrap the output in a + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate); + builder.CloseComponent(); + } + } + + private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteViewCore.RouteData), RouteData); + builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate); + builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate); + builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate); + builder.CloseComponent(); + } + + private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), DefaultLayout); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), content); + builder.CloseComponent(); + } + + private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState) + { + var content = NotAuthorized ?? _defaultNotAuthorizedContent; + RenderContentInDefaultLayout(builder, content(authenticationState)); + } + + private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder) + { + var content = Authorizing ?? _defaultAuthorizingContent; + RenderContentInDefaultLayout(builder, content); + } + + private class AuthorizeRouteViewCore : AuthorizeViewCore + { + [Parameter] + public RouteData RouteData { get; set; } + + protected override IAuthorizeData[] GetAuthorizeData() + => AttributeAuthorizeDataCache.GetAuthorizeDataForType(RouteData.PageType); + } + } +} diff --git a/src/Components/Components/src/Auth/AuthorizeViewCore.cs b/src/Components/Components/src/Auth/AuthorizeViewCore.cs index 05ed8817ed..cdbce6e8d2 100644 --- a/src/Components/Components/src/Auth/AuthorizeViewCore.cs +++ b/src/Components/Components/src/Auth/AuthorizeViewCore.cs @@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Components /// protected override void BuildRenderTree(RenderTreeBuilder builder) { + // We're using the same sequence number for each of the content items here + // so that we can update existing instances if they are the same shape if (currentAuthenticationState == null) { builder.AddContent(0, Authorizing); @@ -59,11 +61,11 @@ namespace Microsoft.AspNetCore.Components else if (isAuthorized) { var authorized = Authorized ?? ChildContent; - builder.AddContent(1, authorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, authorized?.Invoke(currentAuthenticationState)); } else { - builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, NotAuthorized?.Invoke(currentAuthenticationState)); } } @@ -102,6 +104,12 @@ namespace Microsoft.AspNetCore.Components private async Task IsAuthorizedAsync(ClaimsPrincipal user) { var authorizeData = GetAuthorizeData(); + if (authorizeData == null) + { + // No authorization applies, so no need to consult the authorization service + return true; + } + EnsureNoAuthenticationSchemeSpecified(authorizeData); var policy = await AuthorizationPolicy.CombineAsync( diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 7e5fd6507a..018c016975 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -171,10 +171,25 @@ namespace Microsoft.AspNetCore.Components _renderHandle = renderHandle; } + /// - /// Method invoked to apply initial or updated parameters to the component. + /// Sets parameters supplied by the component's parent in the render tree. /// - /// The parameters to apply. + /// The parameters. + /// A that completes when the component has finished updating and rendering itself. + /// + /// + /// The method should be passed the entire set of parameter values each + /// time is called. It not required that the caller supply a parameter + /// value for all parameters that are logically understood by the component. + /// + /// + /// The default implementation of will set the value of each property + /// decorated with or that has + /// a corresponding value in the . Parameters that do not have a corresponding value + /// will be unchanged. + /// + /// public virtual Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index aebf96bc06..bf5de30d2a 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components ( propertyName: property.Name, propertyType: property.PropertyType, - setter: MemberAssignment.CreatePropertySetter(type, property) + setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false) )).ToArray(); return Initialize; diff --git a/src/Components/Components/src/IComponent.cs b/src/Components/Components/src/IComponent.cs index ec1059b1e6..936cd37944 100644 --- a/src/Components/Components/src/IComponent.cs +++ b/src/Components/Components/src/IComponent.cs @@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Components /// /// The parameters. /// A that completes when the component has finished updating and rendering itself. + /// + /// The method should be passed the entire set of parameter values each + /// time is called. It not required that the caller supply a parameter + /// value for all parameters that are logically understood by the component. + /// Task SetParametersAsync(ParameterView parameters); } } diff --git a/src/Components/Components/src/LayoutView.cs b/src/Components/Components/src/LayoutView.cs new file mode 100644 index 0000000000..7b88d181f0 --- /dev/null +++ b/src/Components/Components/src/LayoutView.cs @@ -0,0 +1,77 @@ +// 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; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified content inside the specified layout and any further + /// nested layouts. + /// + public class LayoutView : IComponent + { + private static readonly RenderFragment EmptyRenderFragment = builder => { }; + + private RenderHandle _renderHandle; + + /// + /// Gets or sets the content to display. + /// + [Parameter] + public RenderFragment ChildContent { get; set; } + + /// + /// Gets or sets the type of the layout in which to display the content. + /// The type must implement and accept a parameter named . + /// + [Parameter] + public Type Layout { get; set; } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + Render(); + return Task.CompletedTask; + } + + private void Render() + { + // In the middle goes the supplied content + var fragment = ChildContent ?? EmptyRenderFragment; + + // Then repeatedly wrap that in each layer of nested layout until we get + // to a layout that has no parent + var layoutType = Layout; + while (layoutType != null) + { + fragment = WrapInLayout(layoutType, fragment); + layoutType = GetParentLayoutType(layoutType); + } + + _renderHandle.Render(fragment); + } + + private static RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) + { + return builder => + { + builder.OpenComponent(0, layoutType); + builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); + builder.CloseComponent(); + }; + } + + private static Type GetParentLayoutType(Type type) + => type.GetCustomAttribute()?.LayoutType; + } +} diff --git a/src/Components/Components/src/PageDisplay.cs b/src/Components/Components/src/PageDisplay.cs deleted file mode 100644 index 07d6d4099d..0000000000 --- a/src/Components/Components/src/PageDisplay.cs +++ /dev/null @@ -1,139 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Components.Auth; -using Microsoft.AspNetCore.Components.Rendering; - -namespace Microsoft.AspNetCore.Components -{ - /// - /// Displays the specified page component, rendering it inside its layout - /// and any further nested layouts, plus applying any authorization rules. - /// - public class PageDisplay : IComponent - { - private RenderHandle _renderHandle; - - /// - /// Gets or sets the type of the page component to display. - /// The type must implement . - /// - [Parameter] - public Type Page { get; set; } - - /// - /// Gets or sets the parameters to pass to the page. - /// - [Parameter] - public IDictionary PageParameters { get; set; } - - /// - /// The content that will be displayed if the user is not authorized. - /// - [Parameter] - public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] - public RenderFragment Authorizing { get; set; } - - /// - public void Attach(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - /// - public Task SetParametersAsync(ParameterView parameters) - { - parameters.SetParameterProperties(this); - Render(); - return Task.CompletedTask; - } - - private void Render() - { - // In the middle goes the requested page - var fragment = (RenderFragment)RenderPageWithParameters; - - // Around that goes an AuthorizeViewCore - fragment = WrapInAuthorizeViewCore(fragment); - - // Then repeatedly wrap that in each layer of nested layout until we get - // to a layout that has no parent - Type layoutType = Page; - while ((layoutType = GetLayoutType(layoutType)) != null) - { - fragment = WrapInLayout(layoutType, fragment); - } - - _renderHandle.Render(fragment); - } - - private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder => - { - builder.OpenComponent(0, layoutType); - builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); - builder.CloseComponent(); - }; - - private void RenderPageWithParameters(RenderTreeBuilder builder) - { - builder.OpenComponent(0, Page); - - if (PageParameters != null) - { - foreach (var kvp in PageParameters) - { - builder.AddAttribute(1, kvp.Key, kvp.Value); - } - } - - builder.CloseComponent(); - } - - private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment) - { - var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page); - if (authorizeData == null) - { - // No authorization, so no need to wrap the fragment - return pageFragment; - } - - // Some authorization data exists, so we do need to wrap the fragment - RenderFragment authorized = context => pageFragment; - return builder => - { - builder.OpenComponent(0); - builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData); - builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorized); - builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorized ?? DefaultNotAuthorized); - builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), Authorizing); - builder.CloseComponent(); - }; - } - - private static Type GetLayoutType(Type type) - => type.GetCustomAttribute()?.LayoutType; - - private class AuthorizeViewWithSuppliedData : AuthorizeViewCore - { - [Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; } - - protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam; - } - - // There has to be some default content. If we render blank by default, developers - // will find it hard to guess why their UI isn't appearing. - private static RenderFragment DefaultNotAuthorized(AuthenticationState authenticationState) - => builder => builder.AddContent(0, "Not authorized"); - } -} diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 5caeab20b1..04f2f8d0c1 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -14,6 +14,9 @@ namespace Microsoft.AspNetCore.Components.Reflection { private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase; + // Right now it's not possible for a component to define a Parameter and a Cascading Parameter with + // the same name. We don't give you a way to express this in code (would create duplicate properties), + // and we don't have the ability to represent it in our data structures. private readonly static ConcurrentDictionary _cachedWritersByType = new ConcurrentDictionary(); @@ -44,6 +47,24 @@ namespace Microsoft.AspNetCore.Components.Reflection ThrowForUnknownIncomingParameterName(targetType, parameterName); throw null; // Unreachable } + else if (writer.Cascading && !parameter.Cascading) + { + // We don't allow you to set a cascading parameter with a non-cascading value. Put another way: + // cascading parameters are not part of the public API of a component, so it's not reasonable + // for someone to set it directly. + // + // If we find a strong reason for this to work in the future we can reverse our decision since + // this throws today. + ThrowForSettingCascadingParameterWithNonCascadingValue(targetType, parameterName); + throw null; // Unreachable + } + else if (!writer.Cascading && parameter.Cascading) + { + // We're giving a more specific error here because trying to set a non-cascading parameter + // with a cascading value is likely deliberate (but not supported), or is a bug in our code. + ThrowForSettingParameterWithCascadingValue(targetType, parameterName); + throw null; // Unreachable + } SetProperty(target, writer, parameterName, parameter.Value); } @@ -62,7 +83,24 @@ namespace Microsoft.AspNetCore.Components.Reflection } var isUnmatchedValue = !writers.WritersByName.TryGetValue(parameterName, out var writer); - if (isUnmatchedValue) + + if ((isUnmatchedValue && parameter.Cascading) || (writer != null && !writer.Cascading && parameter.Cascading)) + { + // Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading + // parameter to be set with a cascading value. + // + // This is likely a bug in our infrastructure or an attempt to deliberately do something unsupported. + ThrowForSettingParameterWithCascadingValue(targetType, parameterName); + throw null; // Unreachable + + } + else if (isUnmatchedValue || + + // Allow unmatched parameters to collide with the names of cascading parameters. This is + // valid because cascading parameter names are not part of the public API. There's no + // way for the user of a component to know what the names of cascading parameters + // are. + (writer.Cascading && !parameter.Cascading)) { unmatched ??= new Dictionary(StringComparer.OrdinalIgnoreCase); unmatched[parameterName] = parameter.Value; @@ -138,6 +176,20 @@ namespace Microsoft.AspNetCore.Components.Reflection } } + private static void ThrowForSettingCascadingParameterWithNonCascadingValue(Type targetType, string parameterName) + { + throw new InvalidOperationException( + $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + + $"but it does not have [{nameof(ParameterAttribute)}] applied."); + } + + private static void ThrowForSettingParameterWithCascadingValue(Type targetType, string parameterName) + { + throw new InvalidOperationException( + $"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set " + + $"using a cascading value."); + } + private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, string parameterName, Dictionary unmatched) { throw new InvalidOperationException( @@ -179,13 +231,14 @@ namespace Microsoft.AspNetCore.Components.Reflection foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { var parameterAttribute = propertyInfo.GetCustomAttribute(); - var isParameter = parameterAttribute != null || propertyInfo.IsDefined(typeof(CascadingParameterAttribute)); + var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null; if (!isParameter) { continue; } - var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo); + var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null); var propertyName = propertyInfo.Name; if (WritersByName.ContainsKey(propertyName)) @@ -213,7 +266,7 @@ namespace Microsoft.AspNetCore.Components.Reflection ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo); } - CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo); + CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false); CaptureUnmatchedValuesPropertyName = propertyInfo.Name; } } diff --git a/src/Components/Components/src/Reflection/IPropertySetter.cs b/src/Components/Components/src/Reflection/IPropertySetter.cs index 575b2e669b..d6a60e2395 100644 --- a/src/Components/Components/src/Reflection/IPropertySetter.cs +++ b/src/Components/Components/src/Reflection/IPropertySetter.cs @@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Components.Reflection { internal interface IPropertySetter { + bool Cascading { get; } + void SetValue(object target, object value); } } diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs index 0ab288cedc..b59d7d2ed7 100644 --- a/src/Components/Components/src/Reflection/MemberAssignment.cs +++ b/src/Components/Components/src/Reflection/MemberAssignment.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.Reflection } } - public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property) + public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading) { if (property.SetMethod == null) { @@ -37,19 +37,23 @@ namespace Microsoft.AspNetCore.Components.Reflection return (IPropertySetter)Activator.CreateInstance( typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType), - property.SetMethod); + property.SetMethod, + cascading); } class PropertySetter : IPropertySetter { private readonly Action _setterDelegate; - public PropertySetter(MethodInfo setMethod) + public PropertySetter(MethodInfo setMethod, bool cascading) { _setterDelegate = (Action)Delegate.CreateDelegate( typeof(Action), setMethod); + Cascading = cascading; } + public bool Cascading { get; } + public void SetValue(object target, object value) { if (value == null) diff --git a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs index 18317ece48..3377e18163 100644 --- a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs +++ b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs @@ -8,9 +8,12 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a range of elements within an instance of . + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// /// The type of the elements in the array + // + // Represents a range of elements within an instance of . public readonly struct ArrayBuilderSegment : IEnumerable { // The following fields are memory mapped to the WASM client. Do not re-order or use auto-properties. diff --git a/src/Components/Components/src/RenderTree/ArrayRange.cs b/src/Components/Components/src/RenderTree/ArrayRange.cs index ec113dbdb8..b27a8d5310 100644 --- a/src/Components/Components/src/RenderTree/ArrayRange.cs +++ b/src/Components/Components/src/RenderTree/ArrayRange.cs @@ -4,9 +4,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a range of elements in an array that are in use. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// - /// The array item type. + /// + // + // Represents a range of elements in an array that are in use. public readonly struct ArrayRange { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs index 9ad92ec31b..da5b3ed3f7 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs @@ -1,13 +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; - namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes changes to a component's render tree between successive renders. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Describes changes to a component's render tree between successive renders. public readonly struct RenderTreeDiff { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs index 96f661924b..68437a7471 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs @@ -6,8 +6,11 @@ using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a single edit operation on a component's render tree. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Represents a single edit operation on a component's render tree. [StructLayout(LayoutKind.Explicit)] public readonly struct RenderTreeEdit { diff --git a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs index c2f3e4aba6..f508760135 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs @@ -4,8 +4,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes the type of a render tree edit operation. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + //Describes the type of a render tree edit operation. public enum RenderTreeEditType: int { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs index f3e003080b..39dd7de74a 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs @@ -8,8 +8,11 @@ using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents an entry in a tree of user interface (UI) items. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Represents an entry in a tree of user interface (UI) items. [StructLayout(LayoutKind.Explicit, Pack = 4)] public readonly struct RenderTreeFrame { diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs index 61d2558305..339a7b6795 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs @@ -4,8 +4,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes the type of a . + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Describes the type of a . public enum RenderTreeFrameType: short { /// diff --git a/src/Components/Components/src/Rendering/Renderer.Log.cs b/src/Components/Components/src/Rendering/Renderer.Log.cs index 3b70f58973..bd65809632 100644 --- a/src/Components/Components/src/Rendering/Renderer.Log.cs +++ b/src/Components/Components/src/Rendering/Renderer.Log.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } } - internal static void DisposingComponent(ILogger logger, ComponentState componentState) + public static void DisposingComponent(ILogger logger, ComponentState componentState) { if (logger.IsEnabled(LogLevel.Debug)) // This is almost always false, so skip the evaluations { @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } } - internal static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs) + public static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs) { _handlingEvent(logger, eventHandlerId, eventArgs?.GetType().Name ?? "null", null); } diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 6e0ed1633e..ed3d075f0a 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.Rendering // Since the task has yielded - process any queued rendering work before we return control // to the caller. - ProcessRenderQueue(); + ProcessPendingRender(); } // Task completed synchronously or is still running. We already processed all of the rendering @@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Rendering /// /// The ID of the component to render. /// A that will supply the updated UI contents. - protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment) + internal void AddToRenderQueue(int componentId, RenderFragment renderFragment) { EnsureSynchronizationContext(); @@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.Rendering if (!_isBatchInProgress) { - ProcessRenderQueue(); + ProcessPendingRender(); } } @@ -398,13 +398,33 @@ namespace Microsoft.AspNetCore.Components.Rendering ? componentState : null; + /// + /// Processses pending renders requests from components if there are any. + /// + protected virtual void ProcessPendingRender() + { + ProcessRenderQueue(); + } + private void ProcessRenderQueue() { + EnsureSynchronizationContext(); + + if (_isBatchInProgress) + { + throw new InvalidOperationException("Cannot start a batch when one is already in progress."); + } + _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; try { + if (_batchBuilder.ComponentRenderQueue.Count == 0) + { + return; + } + // Process render queue until empty while (_batchBuilder.ComponentRenderQueue.Count > 0) { @@ -423,6 +443,7 @@ namespace Microsoft.AspNetCore.Components.Rendering { // Ensure we catch errors while running the render functions of the components. HandleException(e); + return; } finally { diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs new file mode 100644 index 0000000000..6f77169e06 --- /dev/null +++ b/src/Components/Components/src/RouteView.cs @@ -0,0 +1,90 @@ +// 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; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified page component, rendering it inside its layout + /// and any further nested layouts. + /// + public class RouteView : IComponent + { + private readonly RenderFragment _renderDelegate; + private readonly RenderFragment _renderPageWithParametersDelegate; + private RenderHandle _renderHandle; + + /// + /// Gets or sets the route data. This determines the page that will be + /// displayed and the parameter values that will be supplied to the page. + /// + [Parameter] + public RouteData RouteData { get; set; } + + /// + /// Gets or sets the type of a layout to be used if the page does not + /// declare any layout. If specified, the type must implement + /// and accept a parameter named . + /// + [Parameter] + public Type DefaultLayout { get; set; } + + public RouteView() + { + // Cache the delegate instances + _renderDelegate = Render; + _renderPageWithParametersDelegate = RenderPageWithParameters; + } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + if (RouteData == null) + { + throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}."); + } + + _renderHandle.Render(_renderDelegate); + return Task.CompletedTask; + } + + /// + /// Renders the component. + /// + /// The . + protected virtual void Render(RenderTreeBuilder builder) + { + var pageLayoutType = RouteData.PageType.GetCustomAttribute()?.LayoutType + ?? DefaultLayout; + + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate); + builder.CloseComponent(); + } + + private void RenderPageWithParameters(RenderTreeBuilder builder) + { + builder.OpenComponent(0, RouteData.PageType); + + foreach (var kvp in RouteData.RouteValues) + { + builder.AddAttribute(1, kvp.Key, kvp.Value); + } + + builder.CloseComponent(); + } + } +} diff --git a/src/Components/Components/src/Routing/RouteContext.cs b/src/Components/Components/src/Routing/RouteContext.cs index 7de5f3c615..7061e9be41 100644 --- a/src/Components/Components/src/Routing/RouteContext.cs +++ b/src/Components/Components/src/Routing/RouteContext.cs @@ -26,6 +26,6 @@ namespace Microsoft.AspNetCore.Components.Routing public Type Handler { get; set; } - public IDictionary Parameters { get; set; } + public IReadOnlyDictionary Parameters { get; set; } } } diff --git a/src/Components/Components/src/Routing/RouteData.cs b/src/Components/Components/src/Routing/RouteData.cs new file mode 100644 index 0000000000..e0da00f0c7 --- /dev/null +++ b/src/Components/Components/src/Routing/RouteData.cs @@ -0,0 +1,46 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Describes information determined during routing that specifies + /// the page to be displayed. + /// + public sealed class RouteData + { + /// + /// Constructs an instance of . + /// + /// The type of the page matching the route, which must implement . + /// The route parameter values extracted from the matched route. + public RouteData(Type pageType, IReadOnlyDictionary routeValues) + { + if (pageType == null) + { + throw new ArgumentNullException(nameof(pageType)); + } + + if (!typeof(IComponent).IsAssignableFrom(pageType)) + { + throw new ArgumentException($"The value must implement {nameof(IComponent)}.", nameof(pageType)); + } + + PageType = pageType; + RouteValues = routeValues ?? throw new ArgumentNullException(nameof(routeValues)); + } + + /// + /// Gets the type of the page matching the route. + /// + public Type PageType { get; } + + /// + /// Gets route parameter values extracted from the matched route. + /// + public IReadOnlyDictionary RouteValues { get; } + } +} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 2049929b4c..42161ff828 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; @@ -11,12 +12,13 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Routing { /// - /// A component that displays whichever other component corresponds to the - /// current navigation location. + /// A component that supplies route data corresponding to the current navigation state. /// public class Router : IComponent, IHandleAfterRender, IDisposable { static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; + static readonly ReadOnlyDictionary _emptyParametersDictionary + = new ReadOnlyDictionary(new Dictionary()); RenderHandle _renderHandle; string _baseUri; @@ -33,25 +35,19 @@ namespace Microsoft.AspNetCore.Components.Routing [Inject] private ILoggerFactory LoggerFactory { get; set; } /// - /// Gets or sets the assembly that should be searched, along with its referenced - /// assemblies, for components matching the URI. + /// Gets or sets the assembly that should be searched for components matching the URI. /// [Parameter] public Assembly AppAssembly { get; set; } /// - /// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route. + /// Gets or sets the content to display when no match is found for the requested route. /// [Parameter] public RenderFragment NotFound { get; set; } /// - /// The content that will be displayed if the user is not authorized. + /// Gets or sets the content to display when a match is found for the requested route. /// - [Parameter] public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] public RenderFragment Authorizing { get; set; } + [Parameter] public RenderFragment Found { get; set; } private RouteTable Routes { get; set; } @@ -69,6 +65,22 @@ namespace Microsoft.AspNetCore.Components.Routing public Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); + + // Found content is mandatory, because even though we could use something like as a + // reasonable default, if it's not declared explicitly in the template then people will have no way + // to discover how to customize this (e.g., to add authorization). + if (Found == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}."); + } + + // NotFound content is mandatory, because even though we could display a default message like "Not found", + // it has to be specified explicitly so that it can also be wrapped in a specific layout + if (NotFound == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}."); + } + Routes = RouteTableFactory.Create(AppAssembly); Refresh(isNavigationIntercepted: false); return Task.CompletedTask; @@ -80,7 +92,7 @@ namespace Microsoft.AspNetCore.Components.Routing NavigationManager.LocationChanged -= OnLocationChanged; } - private string StringUntilAny(string str, char[] chars) + private static string StringUntilAny(string str, char[] chars) { var firstIndex = str.IndexOfAny(chars); return firstIndex < 0 @@ -88,17 +100,6 @@ namespace Microsoft.AspNetCore.Components.Routing : str.Substring(0, firstIndex); } - /// - protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters) - { - builder.OpenComponent(0, typeof(PageDisplay)); - builder.AddAttribute(1, nameof(PageDisplay.Page), handler); - builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters); - builder.AddAttribute(3, nameof(PageDisplay.NotAuthorized), NotAuthorized); - builder.AddAttribute(4, nameof(PageDisplay.Authorizing), Authorizing); - builder.CloseComponent(); - } - private void Refresh(bool isNavigationIntercepted) { var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute); @@ -116,16 +117,19 @@ namespace Microsoft.AspNetCore.Components.Routing Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); - _renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters)); + var routeData = new RouteData( + context.Handler, + context.Parameters ?? _emptyParametersDictionary); + _renderHandle.Render(Found(routeData)); } else { - if (!isNavigationIntercepted && NotFound != null) + if (!isNavigationIntercepted) { Log.DisplayingNotFound(_logger, locationPath, _baseUri); // We did not find a Component that matches the route. - // Only show the NotFound if the application developer programatically got us here i.e we did not + // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. _renderHandle.Render(NotFound); } diff --git a/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs new file mode 100644 index 0000000000..792132e0d0 --- /dev/null +++ b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs @@ -0,0 +1,356 @@ +// 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.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class AuthorizeRouteViewTest + { + private readonly static IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); + private readonly TestAuthenticationStateProvider _authenticationStateProvider; + private readonly TestRenderer _renderer; + private readonly RouteView _authorizeRouteViewComponent; + private readonly int _authorizeRouteViewComponentId; + private readonly TestAuthorizationService _testAuthorizationService; + + public AuthorizeRouteViewTest() + { + _authenticationStateProvider = new TestAuthenticationStateProvider(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult( + new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + _testAuthorizationService = new TestAuthorizationService(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(_authenticationStateProvider); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_testAuthorizationService); + + _renderer = new TestRenderer(serviceCollection.BuildServiceProvider()); + _authorizeRouteViewComponent = new AuthorizeRouteView(); + _authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent); + } + + [Fact] + public void WhenAuthorized_RendersPageInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary + { + { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" } + }); + _testAuthorizationService.NextResult = AuthorizationResult.Success(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Component(batch.ReferenceFrames[edit.ReferenceFrameIndex]); + }, + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Assert: renders page + var pageDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(pageDiff.Edits, + edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!")); + } + + [Fact] + public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Not authorized"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + + // Act + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Go away, Bert"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Authorizing..."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Act 2: updates when authorization completes + authStateTcs.SetResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + await firstRenderTask; + + // Assert 2: Only the layout is updated + batch = _renderer.Batches.Skip(1).Single(); + var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId); + Assert.Collection(nonEmptyDiff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert"); + }); + } + + [Fact] + public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customAuthorizing = + builder => builder.AddContent(0, "Hold on, we're checking your papers."); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.Authorizing), customAuthorizing }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState() + { + // Arrange/Act + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData } + })); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the hierarchy inside the AuthorizeRouteView, which contains its + // own CascadingAuthenticationState + component => Assert.IsType(component), + component => Assert.IsType>>(component), + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState() + { + // Arrange + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState( + _authenticationStateProvider.CurrentAuthStateTask, + routeData); + var rootComponentId = _renderer.AssignRootComponentId(rootComponent); + + // Act + _renderer.RenderRootComponent(rootComponentId); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the externally-supplied cascading value + component => Assert.IsType>>(component), + component => Assert.IsType(component), + + // This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a + // further CascadingAuthenticationState + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void UpdatesOutputWhenRouteDataChanges() + { + // Arrange/Act 1: Start on some route + // Not asserting about the initial output, as that is covered by other tests + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Act 2: Move to another route + var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData2 }, + }))); + + // Assert: we retain the layout instance, and mutate its contents + Assert.True(render2Task.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch2 = _renderer.Batches[1]; + var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Collection(diff.Edits, + edit => + { + // Inside the layout, we add the new content + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized"); + }, + edit => + { + // ... and remove the old content + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + }); + } + + private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text) + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex]; + AssertFrame.Text(referenceFrame, text); + } + + class TestPageWithNoAuthorization : ComponentBase { } + + [Authorize] + class TestPageRequiringAuthorization : ComponentBase + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message: {Message}"); + } + } + + class TestLayout : LayoutComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent + { + private readonly Task _authenticationState; + private readonly RouteData _routeData; + + public AuthorizeRouteViewWithExistingCascadedAuthenticationState( + Task authenticationState, + RouteData routeData) + { + _authenticationState = authenticationState; + _routeData = routeData; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent>>(0); + builder.AddAttribute(1, nameof(CascadingValue.Value), _authenticationState); + builder.AddAttribute(2, nameof(CascadingValue.ChildContent), (RenderFragment)(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteView.RouteData), _routeData); + builder.CloseComponent(); + })); + builder.CloseComponent(); + } + } + } +} diff --git a/src/Components/Components/test/Auth/AuthorizeViewTest.cs b/src/Components/Components/test/Auth/AuthorizeViewTest.cs index 4eebc96a08..c331e9bbd5 100644 --- a/src/Components/Components/test/Auth/AuthorizeViewTest.cs +++ b/src/Components/Components/test/Auth/AuthorizeViewTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Claims; @@ -332,15 +331,9 @@ namespace Microsoft.AspNetCore.Components Assert.Equal(2, renderer.Batches.Count); var batch2 = renderer.Batches[1]; var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single(); - Assert.Collection(diff2.Edits, - edit => + Assert.Collection(diff2.Edits, edit => { - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(0, edit.SiblingIndex); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.SiblingIndex); AssertFrame.Text( batch2.ReferenceFrames[edit.ReferenceFrameIndex], @@ -514,15 +507,6 @@ namespace Microsoft.AspNetCore.Components => Task.FromResult(new AuthenticationState( new ClaimsPrincipal(new TestIdentity { Name = username }))); - class TestIdentity : IIdentity - { - public string AuthenticationType => "Test"; - - public bool IsAuthenticated => true; - - public string Name { get; set; } - } - public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService) { var serviceCollection = new ServiceCollection(); @@ -531,52 +515,6 @@ namespace Microsoft.AspNetCore.Components return new TestRenderer(serviceCollection.BuildServiceProvider()); } - private class TestAuthorizationService : IAuthorizationService - { - public AuthorizationResult NextResult { get; set; } - = AuthorizationResult.Failed(); - - public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } - = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) - { - AuthorizeCalls.Add((user, resource, requirements)); - - // The TestAuthorizationService doesn't actually apply any authorization requirements - // It just returns the specified NextResult, since we're not trying to test the logic - // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired - // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. - return Task.FromResult(NextResult); - } - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) - => throw new NotImplementedException(); - } - - private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider - { - private readonly AuthorizationOptions options = new AuthorizationOptions(); - - public Task GetDefaultPolicyAsync() - => Task.FromResult(options.DefaultPolicy); - - public Task GetFallbackPolicyAsync() - => Task.FromResult(options.FallbackPolicy); - - public Task GetPolicyAsync(string policyName) => Task.FromResult( - new AuthorizationPolicy(new[] - { - new TestPolicyRequirement { PolicyName = policyName } - }, - new[] { $"TestScheme:{policyName}" })); - } - - public class TestPolicyRequirement : IAuthorizationRequirement - { - public string PolicyName { get; set; } - } - public class AuthorizeViewCoreWithScheme : AuthorizeViewCore { protected override IAuthorizeData[] GetAuthorizeData() diff --git a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs index 69bb527d04..ec347b01c2 100644 --- a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs +++ b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Components { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert")) }; @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components // Arrange: Service var services = new ServiceCollection(); var authStateTaskCompletionSource = new TaskCompletionSource(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = authStateTaskCompletionSource.Task }; @@ -123,7 +123,7 @@ namespace Microsoft.AspNetCore.Components { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null)) }; @@ -190,21 +190,6 @@ namespace Microsoft.AspNetCore.Components } } - class TestAuthStateProvider : AuthenticationStateProvider - { - public Task CurrentAuthStateTask { get; set; } - - public override Task GetAuthenticationStateAsync() - { - return CurrentAuthStateTask; - } - - internal void TriggerAuthenticationStateChanged(Task authState) - { - NotifyAuthenticationStateChanged(authState); - } - } - public static AuthenticationState CreateAuthenticationState(string username) => new AuthenticationState(new ClaimsPrincipal(username == null ? new ClaimsIdentity() diff --git a/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs new file mode 100644 index 0000000000..6a84916e93 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs @@ -0,0 +1,22 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthenticationStateProvider : AuthenticationStateProvider + { + public Task CurrentAuthStateTask { get; set; } + + public override Task GetAuthenticationStateAsync() + { + return CurrentAuthStateTask; + } + + internal void TriggerAuthenticationStateChanged(Task authState) + { + NotifyAuthenticationStateChanged(authState); + } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs new file mode 100644 index 0000000000..b3e903fdb3 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs @@ -0,0 +1,31 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private readonly AuthorizationOptions options = new AuthorizationOptions(); + + public Task GetDefaultPolicyAsync() + => Task.FromResult(options.DefaultPolicy); + + public Task GetFallbackPolicyAsync() + => Task.FromResult(options.FallbackPolicy); + + public Task GetPolicyAsync(string policyName) => Task.FromResult( + new AuthorizationPolicy(new[] + { + new TestPolicyRequirement { PolicyName = policyName } + }, + new[] { $"TestScheme:{policyName}" })); + } + + public class TestPolicyRequirement : IAuthorizationRequirement + { + public string PolicyName { get; set; } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationService.cs b/src/Components/Components/test/Auth/TestAuthorizationService.cs new file mode 100644 index 0000000000..d6cc1ff11a --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationService.cs @@ -0,0 +1,34 @@ +// 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; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationService : IAuthorizationService + { + public AuthorizationResult NextResult { get; set; } + = AuthorizationResult.Failed(); + + public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } + = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + { + AuthorizeCalls.Add((user, resource, requirements)); + + // The TestAuthorizationService doesn't actually apply any authorization requirements + // It just returns the specified NextResult, since we're not trying to test the logic + // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired + // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. + return Task.FromResult(NextResult); + } + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => throw new NotImplementedException(); + } +} diff --git a/src/Components/Components/test/Auth/TestIdentity.cs b/src/Components/Components/test/Auth/TestIdentity.cs new file mode 100644 index 0000000000..d650c53fe6 --- /dev/null +++ b/src/Components/Components/test/Auth/TestIdentity.cs @@ -0,0 +1,16 @@ +// 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.Security.Principal; + +namespace Microsoft.AspNetCore.Components +{ + public class TestIdentity : IIdentity + { + public string AuthenticationType => "Test"; + + public bool IsAuthenticated => true; + + public string Name { get; set; } + } +} diff --git a/src/Components/Components/test/LayoutViewTest.cs b/src/Components/Components/test/LayoutViewTest.cs new file mode 100644 index 0000000000..592cb7e62d --- /dev/null +++ b/src/Components/Components/test/LayoutViewTest.cs @@ -0,0 +1,326 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class LayoutViewTest + { + private readonly TestRenderer _renderer; + private readonly LayoutView _layoutViewComponent; + private readonly int _layoutViewComponentId; + + public LayoutViewTest() + { + _renderer = new TestRenderer(); + _layoutViewComponent = new LayoutView(); + _layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent); + } + + [Fact] + public void GivenNoParameters_RendersNothing() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty)); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Empty(frames); + } + + [Fact] + public void GivenContentButNoLayout_RendersContent() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Collection(frames, + frame => AssertFrame.Text(frame, "Hello", 123), + frame => AssertFrame.Text(frame, "Goodbye", 456)); + } + + [Fact] + public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) } + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideNestedLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + + var nestedLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable(); + Assert.Collection(nestedLayoutFrames, + frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2)); + } + + [Fact] + public void CanChangeContentWithSameLayout() + { + // Arrange + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Initial content"); + })} + }))); + + // Act + Assert.True(setParametersTask.IsCompletedSuccessfully); + _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Changed content"); + })} + }))); + + // Assert + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(0, batch.DisposedComponentIDs.Count); + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes + diff => + { + // NestedLayout rerendered, patching content in place + Assert.Collection(diff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Changed content", + sequence: 0); + }); + }); + } + + [Fact] + public void CanChangeLayout() + { + // Arrange + var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Some content"); + })} + }))); + Assert.True(setParametersTask1.IsCompletedSuccessfully); + + // Act + var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(OtherNestedLayout) }, + }))); + + // Assert + Assert.True(setParametersTask2.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposes NestedLayout + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => + { + // RootLayout rerendered, changing child + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Component( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + sequence: 0); + }); + }, + diff => + { + // Inserts new OtherNestedLayout + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(0, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout starts here"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Some content"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout ends here"); + }); + }); + } + + private class RootLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + if (Body == null) + { + // Prove that we don't expect layouts to tolerate null values for Body + throw new InvalidOperationException("Got a null body when not expecting it"); + } + + builder.AddContent(0, "RootLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "RootLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class NestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "NestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "NestedLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class OtherNestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherNestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherNestedLayout ends here"); + } + } + } +} diff --git a/src/Components/Components/test/PageDisplayTest.cs b/src/Components/Components/test/PageDisplayTest.cs deleted file mode 100644 index 49df7d47fc..0000000000 --- a/src/Components/Components/test/PageDisplayTest.cs +++ /dev/null @@ -1,269 +0,0 @@ -// 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 Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Test.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Test -{ - public class PageDisplayTest - { - private TestRenderer _renderer = new TestRenderer(); - private PageDisplay _pageDisplayComponent = new PageDisplay(); - private int _pageDisplayComponentId; - - public PageDisplayTest() - { - _renderer = new TestRenderer(); - _pageDisplayComponent = new PageDisplay(); - _pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent); - } - - [Fact] - public void DisplaysComponentInsideLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - diff => - { - // First is the LayoutDisplay component, which contains a RootLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Component( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex]); - }, - diff => - { - // ... then a RootLayout which contains a ComponentWithLayout - // First is the LayoutDisplay component, which contains a RootLayout - Assert.Collection(diff.Edits, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout starts here"); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout ends here"); - }); - }, - diff => - { - // ... then the ComponentWithLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithLayout)} is here."); - }); - } - - [Fact] - public void DisplaysComponentInsideNestedLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - // First, a LayoutDisplay containing a RootLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex]), - // Then a RootLayout containing a NestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then a NestedLayout containing a ComponentWithNestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then the ComponentWithNestedLayout - diff => AssertFrame.Text( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here.")); - } - - [Fact] - public void CanChangeDisplayedPageWithSameLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new one - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(DifferentComponentWithLayout)} is here."); - }); - } - - [Fact] - public void CanChangeDisplayedPageWithDifferentLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new nested layout - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New nested layout rendered - var edit = diff.Edits[1]; - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - diff => - { - // New inner page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here."); - }); - } - - private class RootLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "RootLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "RootLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class NestedLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "NestedLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "NestedLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class ComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithLayout)} is here."); - } - - [Layout(typeof(RootLayout))] - private class DifferentComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(DifferentComponentWithLayout)} is here."); - } - - [Layout(typeof(NestedLayout))] - private class ComponentWithNestedLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithNestedLayout)} is here."); - } - } -} diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 1166f19617..bf2f38af5a 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -5,10 +5,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; namespace Microsoft.AspNetCore.Components @@ -97,23 +95,58 @@ namespace Microsoft.AspNetCore.Components } [Fact] - public void IncomingParameterMatchesNoDeclaredParameter_Throws() + public void IncomingCascadingValueMatchesCascadingParameter_SetsValue() { // Arrange - var target = new HasPropertyWithoutParameterAttribute(); - var parameters = new ParameterViewBuilder - { - { "AnyOtherKey", 123 }, - }.Build(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasCascadingParameter.Cascading), "hi", cascading: true); + var parameters = builder.Build(); + + var target = new HasCascadingParameter(); // Act - var ex = Assert.Throws( - () => parameters.SetParameterProperties(target)); + parameters.SetParameterProperties(target); + + // Assert + Assert.Equal("hi", target.Cascading); + } + + [Fact] + public void NoIncomingCascadingValueMatchesDeclaredCascadingParameter_LeavesValueUnchanged() + { + // Arrange + var builder = new ParameterViewBuilder(); + var parameters = builder.Build(); + + var target = new HasCascadingParameter() + { + Cascading = "bye", + }; + + // Act + parameters.SetParameterProperties(target); + + // Assert + Assert.Equal("bye", target.Cascading); + } + + [Fact] + public void IncomingCascadingValueMatchesNoDeclaredParameter_Throws() + { + // Arrange + var builder = new ParameterViewBuilder(); + builder.Add("SomethingElse", "hi", cascading: true); + var parameters = builder.Build(); + + var target = new HasCascadingParameter(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); // Assert Assert.Equal( - $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' does not have a property " + - $"matching the name 'AnyOtherKey'.", + $"Object of type '{typeof(HasCascadingParameter).FullName}' does not have a property " + + $"matching the name 'SomethingElse'.", ex.Message); } @@ -139,6 +172,45 @@ namespace Microsoft.AspNetCore.Components ex.Message); } + [Fact] + public void IncomingNonCascadingValueMatchesCascadingParameter_Throws() + { + // Arrange + var target = new HasCascadingParameter(); + var parameters = new ParameterViewBuilder + { + { nameof(HasCascadingParameter.Cascading), 123 }, + }.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"Object of type '{typeof(HasCascadingParameter).FullName}' has a property matching the name '{nameof(HasCascadingParameter.Cascading)}', " + + $"but it does not have [{nameof(ParameterAttribute)}] applied.", + ex.Message); + } + + [Fact] + public void IncomingCascadingValueMatchesNonCascadingParameter_Throws() + { + // Arrange + var target = new HasInstanceProperties(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasInstanceProperties.IntProp), 16, cascading: true); + var parameters = builder.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"The property '{nameof(HasInstanceProperties.IntProp)}' on component type '{typeof(HasInstanceProperties).FullName}' " + + $"cannot be set using a cascading value.", + ex.Message); + } + [Fact] public void SettingCaptureUnmatchedValuesParameterExplicitlyWorks() { @@ -275,6 +347,51 @@ namespace Microsoft.AspNetCore.Components ex.Message); } + [Fact] + public void IncomingNonCascadingValueMatchesCascadingParameter_WithCaptureUnmatchedValues_DoesNotThrow() + { + // Arrange + var target = new HasCaptureUnmatchedValuesPropertyAndCascadingParameter() + { + Cascading = "bye", + }; + var parameters = new ParameterViewBuilder + { + { nameof(HasCaptureUnmatchedValuesPropertyAndCascadingParameter.Cascading), "hi" }, + }.Build(); + + // Act + parameters.SetParameterProperties(target); + + Assert.Collection( + target.CaptureUnmatchedValues, + kvp => + { + Assert.Equal(nameof(HasCaptureUnmatchedValuesPropertyAndCascadingParameter.Cascading), kvp.Key); + Assert.Equal("hi", kvp.Value); + }); + Assert.Equal("bye", target.Cascading); + } + + [Fact] + public void IncomingCascadingValueMatchesNonCascadingParameter_WithCaptureUnmatchedValues_Throws() + { + // Arrange + var target = new HasCaptureUnmatchedValuesProperty(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasInstanceProperties.IntProp), 16, cascading: true); + var parameters = builder.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"The property '{nameof(HasCaptureUnmatchedValuesProperty.IntProp)}' on component type '{typeof(HasCaptureUnmatchedValuesProperty).FullName}' " + + $"cannot be set using a cascading value.", + ex.Message); + } + [Fact] public void IncomingParameterValueMismatchesDeclaredParameterType_Throws() { @@ -397,6 +514,11 @@ namespace Microsoft.AspNetCore.Components } } + class HasCascadingParameter + { + [CascadingParameter] public string Cascading { get; set; } + } + class HasPropertyWithoutParameterAttribute { internal int IntProp { get; set; } @@ -436,6 +558,12 @@ namespace Microsoft.AspNetCore.Components [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary CaptureUnmatchedValues { get; set; } } + class HasCaptureUnmatchedValuesPropertyAndCascadingParameter + { + [CascadingParameter] public string Cascading { get; set; } + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary CaptureUnmatchedValues { get; set; } + } + class HasDupliateCaptureUnmatchedValuesProperty { [Parameter(CaptureUnmatchedValues = true)] public Dictionary CaptureUnmatchedValuesProp1 { get; set; } @@ -449,11 +577,11 @@ namespace Microsoft.AspNetCore.Components class ParameterViewBuilder : IEnumerable { - private readonly List<(string Name, object Value)> _keyValuePairs - = new List<(string, object)>(); + private readonly List<(string Name, object Value, bool Cascading)> _keyValuePairs + = new List<(string, object, bool)>(); - public void Add(string name, object value) - => _keyValuePairs.Add((name, value)); + public void Add(string name, object value, bool cascading = false) + => _keyValuePairs.Add((name, value, cascading)); public IEnumerator GetEnumerator() => throw new NotImplementedException(); @@ -461,13 +589,56 @@ namespace Microsoft.AspNetCore.Components public ParameterView Build() { var builder = new RenderTreeBuilder(); + builder.OpenComponent(0); foreach (var kvp in _keyValuePairs) { - builder.AddAttribute(1, kvp.Name, kvp.Value); + if (!kvp.Cascading) + { + builder.AddAttribute(1, kvp.Name, kvp.Value); + } } builder.CloseComponent(); - return new ParameterView(builder.GetFrames().Array, ownerIndex: 0); + + var view = new ParameterView(builder.GetFrames().Array, ownerIndex: 0); + + var cascadingParameters = new List(); + foreach (var kvp in _keyValuePairs) + { + if (kvp.Cascading) + { + cascadingParameters.Add(new CascadingParameterState(kvp.Name, new TestCascadingValueProvider(kvp.Value))); + } + } + + return view.WithCascadingParameters(cascadingParameters); + } + } + + private class TestCascadingValueProvider : ICascadingValueComponent + { + public TestCascadingValueProvider(object value) + { + CurrentValue = value; + } + + public object CurrentValue { get; } + + public bool CurrentValueIsFixed => throw new NotImplementedException(); + + public bool CanSupplyValue(Type valueType, string valueName) + { + throw new NotImplementedException(); + } + + public void Subscribe(ComponentState subscriber) + { + throw new NotImplementedException(); + } + + public void Unsubscribe(ComponentState subscriber) + { + throw new NotImplementedException(); } } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index f895c47f10..8d3ce23570 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3395,6 +3395,27 @@ namespace Microsoft.AspNetCore.Components.Test } } + [Fact] + public void CannotStartOverlappingBatches() + { + // Arrange + var renderer = new InvalidRecursiveRenderer(); + var component = new CallbackOnRenderComponent(() => + { + // The renderer disallows one batch to be started inside another, because that + // would violate all kinds of state tracking invariants. It's not something that + // would ever happen except if you subclass the renderer and do something unsupported + // that commences batches from inside each other. + renderer.ProcessPendingRender(); + }); + var componentId = renderer.AssignRootComponentId(component); + + // Act/Assert + var ex = Assert.Throws( + () => renderer.RenderRootComponent(componentId)); + Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message); + } + private class NoOpRenderer : Renderer { public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance) @@ -4109,5 +4130,24 @@ namespace Microsoft.AspNetCore.Components.Test private class DerivedEventArgs : EventArgs { } + + class CallbackOnRenderComponent : AutoRenderComponent + { + private readonly Action _callback; + + public CallbackOnRenderComponent(Action callback) + { + _callback = callback; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + => _callback(); + } + + class InvalidRecursiveRenderer : TestRenderer + { + public new void ProcessPendingRender() + => base.ProcessPendingRender(); + } } } diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs new file mode 100644 index 0000000000..05b1c8b20f --- /dev/null +++ b/src/Components/Components/test/RouteViewTest.cs @@ -0,0 +1,209 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class RouteViewTest + { + private readonly TestRenderer _renderer; + private readonly RouteView _routeViewComponent; + private readonly int _routeViewComponentId; + + public RouteViewTest() + { + _renderer = new TestRenderer(); + _routeViewComponent = new RouteView(); + _routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent); + } + + [Fact] + public void ThrowsIfNoRouteDataSupplied() + { + var ex = Assert.Throws(() => + { + // Throws synchronously, so no need to await + _ = _routeViewComponent.SetParametersAsync(ParameterView.Empty); + }); + + + Assert.Equal($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteView.RouteData)}.", ex.Message); + } + + [Fact] + public void RendersPageInsideLayoutView() + { + // Arrange + var routeParams = new Dictionary + { + { nameof(ComponentWithLayout.Message), "Test message" } + }; + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: RouteView renders LayoutView + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + + // Assert: LayoutView renders TestLayout + var layoutViewComponentId = batch.GetComponentFrames().Single().ComponentId; + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + // Assert: TestLayout renders page + var testLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var testLayoutFrames = _renderer.GetCurrentRenderTreeFrames(testLayoutComponentId).AsEnumerable(); + Assert.Collection(testLayoutFrames, + frame => AssertFrame.Text(frame, "Layout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Component(frame, sequence: 0, subtreeLength: 2), + frame => AssertFrame.Attribute(frame, nameof(ComponentWithLayout.Message), "Test message", sequence: 1), + frame => AssertFrame.Text(frame, "Layout ends here", sequence: 2)); + + // Assert: page itself is rendered, having received parameters from the original route data + var pageComponentId = batch.GetComponentFrames().Single().ComponentId; + var pageFrames = _renderer.GetCurrentRenderTreeFrames(pageComponentId).AsEnumerable(); + Assert.Collection(pageFrames, + frame => AssertFrame.Text(frame, "Hello from the page with message 'Test message'", sequence: 0)); + + // Assert: nothing else was rendered + Assert.Equal(4, batch.DiffsInOrder.Count); + } + + [Fact] + public void UsesDefaultLayoutIfNoneSetOnPage() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses default layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(OtherLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void UsesNoLayoutIfNoneSetOnPageAndNoDefaultSet() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: uses no layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)null, sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void PageLayoutSupersedesDefaultLayout() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses layout specified by page + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + private class ComponentWithoutLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + [Layout(typeof(TestLayout))] + private class ComponentWithLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + private class TestLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + private class OtherLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherLayout ends here"); + } + } + } +} diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets index bd6e405829..b992960cc3 100644 --- a/src/Components/Directory.Build.targets +++ b/src/Components/Directory.Build.targets @@ -5,6 +5,16 @@ + + + + diff --git a/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs b/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs index 7a89318f43..5440426852 100644 --- a/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs +++ b/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs @@ -33,6 +33,21 @@ namespace Microsoft.AspNetCore.Components.Server public int DisconnectedCircuitMaxRetained { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan DisconnectedCircuitRetentionPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan JSInteropDefaultCallTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public int MaxBufferedUnacknowledgedRenderBatches { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public abstract partial class RevalidatingServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider, System.IDisposable + { + public RevalidatingServerAuthenticationStateProvider(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + protected abstract System.TimeSpan RevalidationInterval { get; } + protected virtual void Dispose(bool disposing) { } + void System.IDisposable.Dispose() { } + protected abstract System.Threading.Tasks.Task ValidateAuthenticationStateAsync(Microsoft.AspNetCore.Components.AuthenticationState authenticationState, System.Threading.CancellationToken cancellationToken); + } + public partial class ServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.AuthenticationStateProvider, Microsoft.AspNetCore.Components.IHostEnvironmentAuthenticationStateProvider + { + public ServerAuthenticationStateProvider() { } + public override System.Threading.Tasks.Task GetAuthenticationStateAsync() { throw null; } + public void SetAuthenticationState(System.Threading.Tasks.Task authenticationStateTask) { } } } namespace Microsoft.AspNetCore.Components.Server.Circuits diff --git a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs index 021272c7d6..a154feb4e6 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs @@ -10,11 +10,13 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class ComponentEndpointConventionBuilder : IHubEndpointConventionBuilder { - private readonly IEndpointConventionBuilder _endpointConventionBuilder; + private readonly IEndpointConventionBuilder _hubEndpoint; + private readonly IEndpointConventionBuilder _disconnectEndpoint; - internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder) + internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint) { - _endpointConventionBuilder = endpointConventionBuilder; + _hubEndpoint = hubEndpoint; + _disconnectEndpoint = disconnectEndpoint; } /// @@ -23,7 +25,8 @@ namespace Microsoft.AspNetCore.Builder /// The convention to add to the builder. public void Add(Action convention) { - _endpointConventionBuilder.Add(convention); + _hubEndpoint.Add(convention); + _disconnectEndpoint.Add(convention); } } } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 8af4ad37e8..d747a69b60 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -292,7 +292,17 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(configureOptions)); } - return new ComponentEndpointConventionBuilder(endpoints.MapHub(path, configureOptions)).AddComponent(componentType, selector); + var hubEndpoint = endpoints.MapHub(path, configureOptions); + + var disconnectEndpoint = endpoints.Map( + (path.EndsWith("/") ? path : path + "/") + "disconnect/", + endpoints.CreateApplicationBuilder().UseMiddleware().Build()) + .WithDisplayName("Blazor disconnect"); + + return new ComponentEndpointConventionBuilder( + hubEndpoint, + disconnectEndpoint) + .AddComponent(componentType, selector); } } } diff --git a/src/Components/Server/src/CircuitDisconnectMiddleware.cs b/src/Components/Server/src/CircuitDisconnectMiddleware.cs new file mode 100644 index 0000000000..ecacf511d7 --- /dev/null +++ b/src/Components/Server/src/CircuitDisconnectMiddleware.cs @@ -0,0 +1,103 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Server +{ + // We use a middlware so that we can use DI. + internal class CircuitDisconnectMiddleware + { + private const string CircuitIdKey = "circuitId"; + + public CircuitDisconnectMiddleware( + ILogger logger, + CircuitRegistry registry, + CircuitIdFactory circuitIdFactory, + RequestDelegate next) + { + Logger = logger; + Registry = registry; + CircuitIdFactory = circuitIdFactory; + Next = next; + } + + public ILogger Logger { get; } + public CircuitRegistry Registry { get; } + public CircuitIdFactory CircuitIdFactory { get; } + public RequestDelegate Next { get; } + + public async Task Invoke(HttpContext context) + { + if (!HttpMethods.IsPost(context.Request.Method)) + { + context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; + return; + } + + var (hasCircuitId, circuitId) = await TryGetCircuitIdAsync(context); + if (!hasCircuitId) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + await TerminateCircuitGracefully(circuitId); + + context.Response.StatusCode = StatusCodes.Status200OK; + } + + private async Task<(bool, string)> TryGetCircuitIdAsync(HttpContext context) + { + try + { + if (!context.Request.HasFormContentType) + { + return (false, null); + } + + var form = await context.Request.ReadFormAsync(); + if (!form.TryGetValue(CircuitIdKey, out var circuitId) || !CircuitIdFactory.ValidateCircuitId(circuitId)) + { + return (false, null); + } + + return (true, circuitId); + } + catch + { + return (false, null); + } + } + + private async Task TerminateCircuitGracefully(string circuitId) + { + try + { + await Registry.Terminate(circuitId); + Log.CircuitTerminatedGracefully(Logger, circuitId); + } + catch (Exception e) + { + Log.UnhandledExceptionInCircuit(Logger, circuitId, e); + } + } + + private class Log + { + private static readonly Action _circuitTerminatedGracefully = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully"); + + private static readonly Action _unhandledExceptionInCircuit = + LoggerMessage.Define(LogLevel.Warning, new EventId(2, "UnhandledExceptionInCircuit"), "Unhandled exception in circuit {CircuitId} while terminating gracefully."); + + public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null); + + public static void UnhandledExceptionInCircuit(ILogger logger, string circuitId, Exception exception) => _unhandledExceptionInCircuit(logger, circuitId, exception); + } + } +} diff --git a/src/Components/Server/src/CircuitOptions.cs b/src/Components/Server/src/CircuitOptions.cs index 30c1b9c407..68ca25c85a 100644 --- a/src/Components/Server/src/CircuitOptions.cs +++ b/src/Components/Server/src/CircuitOptions.cs @@ -64,5 +64,17 @@ namespace Microsoft.AspNetCore.Components.Server /// Defaults to 1 minute. /// public TimeSpan JSInteropDefaultCallTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the maximum number of render batches that a circuit will buffer until an acknowledgement for the batch is + /// received. + /// + /// + /// When the limit of buffered render batches is reached components will stop rendering and will wait until either the + /// circuit is disconnected and disposed or at least one batch gets acknowledged. + /// + /// + /// Defaults to 10. + public int MaxBufferedUnacknowledgedRenderBatches { get; set; } = 10; } } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 563cacbff6..c2a38e25b9 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -230,7 +230,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits catch (Exception ex) { // We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw - // however, we still want this to get logged if we do. + // however, we still want this to get logged if we do. UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); } } diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs index 9fe25c5b68..a5c970c4af 100644 --- a/src/Components/Server/src/Circuits/CircuitRegistry.cs +++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs @@ -81,15 +81,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - public void PermanentDisconnect(CircuitHost circuitHost) - { - if (ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _)) - { - Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId); - circuitHost.Client.SetDisconnected(); - } - } - public virtual Task DisconnectAsync(CircuitHost circuitHost, string connectionId) { Log.CircuitDisconnectStarted(_logger, circuitHost.CircuitId, connectionId); @@ -297,6 +288,29 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } + public ValueTask Terminate(string circuitId) + { + CircuitHost circuitHost; + DisconnectedCircuitEntry entry = default; + lock (CircuitRegistryLock) + { + if (ConnectedCircuits.TryGetValue(circuitId, out circuitHost) || DisconnectedCircuits.TryGetValue(circuitId, out entry)) + { + circuitHost ??= entry.CircuitHost; + DisconnectedCircuits.Remove(circuitHost.CircuitId); + ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _); + Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId); + circuitHost.Client.SetDisconnected(); + } + else + { + return default; + } + } + + return circuitHost?.DisposeAsync() ?? default; + } + private readonly struct DisconnectedCircuitEntry { public DisconnectedCircuitEntry(CircuitHost circuitHost, CancellationTokenSource tokenSource) diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index d353d5dc2c..f3463301b1 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.Components.Routing; @@ -13,8 +14,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components.Server.Circuits { @@ -24,16 +25,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly CircuitIdFactory _circuitIdFactory; + private readonly CircuitOptions _options; public DefaultCircuitFactory( IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory, - CircuitIdFactory circuitIdFactory) + CircuitIdFactory circuitIdFactory, + IOptions options) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _loggerFactory = loggerFactory; _logger = _loggerFactory.CreateLogger(); _circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory)); + _options = options.Value; } public override CircuitHost CreateCircuitHost( @@ -54,13 +58,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits jsRuntime.Initialize(client); componentContext.Initialize(client); - var authenticationStateProvider = scope.ServiceProvider.GetService() as IHostEnvironmentAuthenticationStateProvider; - if (authenticationStateProvider != null) - { - var authenticationState = new AuthenticationState(httpContext.User); // TODO: Get this from the hub connection context instead - authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); - } - var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService(); if (client.Connected) @@ -80,6 +77,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits scope.ServiceProvider, _loggerFactory, rendererRegistry, + _options, jsRuntime, client, encoder, diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 8b9ebb341a..096ee33ff0 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.Linq; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Internal; @@ -23,8 +23,10 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering private readonly IJSRuntime _jsRuntime; private readonly CircuitClientProxy _client; + private readonly CircuitOptions _options; private readonly RendererRegistry _rendererRegistry; private readonly ILogger _logger; + internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue(); private long _nextRenderId = 1; private bool _disposing = false; @@ -40,6 +42,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering IServiceProvider serviceProvider, ILoggerFactory loggerFactory, RendererRegistry rendererRegistry, + CircuitOptions options, IJSRuntime jsRuntime, CircuitClientProxy client, HtmlEncoder encoder, @@ -49,13 +52,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering _rendererRegistry = rendererRegistry; _jsRuntime = jsRuntime; _client = client; + _options = options; Id = _rendererRegistry.Add(this); _logger = logger; } - internal ConcurrentQueue UnacknowledgedRenderBatches = new ConcurrentQueue(); - public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); public int Id { get; } @@ -81,6 +83,34 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering return RenderRootComponentAsync(componentId); } + protected override void ProcessPendingRender() + { + if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches) + { + // If we got here it means we are at max capacity, so we don't want to actually process the queue, + // as we have a client that is not acknowledging render batches fast enough (something we consider needs + // to be fast). + // The result is something as follows: + // Lets imagine an extreme case where the server produces a new batch every milisecond. + // Lets say the client is able to ACK a batch every 100 miliseconds. + // When the app starts the client might see the sequence 0->(MaxUnacknowledgedRenderBatches-1) and then + // after 100 miliseconds it sees it jump to 1xx, then to 2xx where xx is something between {0..99} the + // reason for this is that the server slows down rendering new batches to as fast as the client can consume + // them. + // Similarly, if a client were to send events at a faster pace than the server can consume them, the server + // would still proces the events, but would not produce new renders until it gets an ack that frees up space + // for a new render. + // We should never see UnacknowledgedRenderBatches.Count > _options.MaxBufferedUnacknowledgedRenderBatches + + // But if we do, it's safer to simply disable the rendering in that case too instead of allowing batches to + Log.FullUnacknowledgedRenderBatchesQueue(_logger); + + return; + } + + base.ProcessPendingRender(); + } + /// protected override void HandleException(Exception exception) { @@ -104,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering { _disposing = true; _rendererRegistry.TryRemove(Id); - while (UnacknowledgedRenderBatches.TryDequeue(out var entry)) + while (_unacknowledgedRenderBatches.TryDequeue(out var entry)) { entry.CompletionSource.TrySetCanceled(); entry.Data.Dispose(); @@ -146,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // Buffer the rendered batches no matter what. We'll send it down immediately when the client // is connected or right after the client reconnects. - UnacknowledgedRenderBatches.Enqueue(pendingRender); + _unacknowledgedRenderBatches.Enqueue(pendingRender); } catch { @@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // All the batches are sent in order based on the fact that SignalR // provides ordering for the underlying messages and that the batches // are always in order. - return Task.WhenAll(UnacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b))); + return Task.WhenAll(_unacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b))); } private async Task WriteBatchBytesAsync(UnacknowledgedRenderBatch pending) @@ -203,12 +233,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // disposed. } - public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull) + public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull) { if (_disposing) { // Disposing so don't do work. - return; + return Task.CompletedTask; } // When clients send acks we know for sure they received and applied the batch. @@ -234,18 +264,21 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // synchronizes calls to hub methods. That is, it won't issue more than one call to this method from the same hub // at the same time on different threads. - if (!UnacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId) + if (!_unacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId) { Log.ReceivedDuplicateBatchAck(_logger, incomingBatchId); + return Task.CompletedTask; } else { var lastBatchId = nextUnacknowledgedBatch.BatchId; // Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch - while (UnacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId) + while (_unacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId) { lastBatchId = nextUnacknowledgedBatch.BatchId; - UnacknowledgedRenderBatches.TryDequeue(out _); + // At this point the queue is definitely not full, we have at least emptied one slot, so we allow a further + // full queue log entry the next time it fills up. + _unacknowledgedRenderBatches.TryDequeue(out _); ProcessPendingBatch(errorMessageOrNull, nextUnacknowledgedBatch); } @@ -253,7 +286,16 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering { HandleException( new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'.")); + return Task.CompletedTask; } + + // Normally we will not have pending renders, but it might happen that we reached the limit of + // available buffered renders and new renders got queued. + // Invoke ProcessBufferedRenderRequests so that we might produce any additional batch that is + // missing. + + // We return the task in here, but the caller doesn't await it. + return Dispatcher.InvokeAsync(() => ProcessPendingRender()); } } @@ -321,6 +363,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering private static readonly Action _completingBatchWithError; private static readonly Action _completingBatchWithoutError; private static readonly Action _receivedDuplicateBatchAcknowledgement; + private static readonly Action _fullUnacknowledgedRenderBatchesQueue; private static class EventIds { @@ -331,6 +374,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public static readonly EventId CompletingBatchWithError = new EventId(104, "CompletingBatchWithError"); public static readonly EventId CompletingBatchWithoutError = new EventId(105, "CompletingBatchWithoutError"); public static readonly EventId ReceivedDuplicateBatchAcknowledgement = new EventId(106, "ReceivedDuplicateBatchAcknowledgement"); + public static readonly EventId FullUnacknowledgedRenderBatchesQueue = new EventId(107, "FullUnacknowledgedRenderBatchesQueue"); } static Log() @@ -369,6 +413,11 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering LogLevel.Debug, EventIds.ReceivedDuplicateBatchAcknowledgement, "Received a duplicate ACK for batch id '{IncomingBatchId}'."); + + _fullUnacknowledgedRenderBatchesQueue = LoggerMessage.Define( + LogLevel.Debug, + EventIds.FullUnacknowledgedRenderBatchesQueue, + "The queue of unacknowledged render batches is full."); } public static void SendBatchDataFailed(ILogger logger, Exception exception) @@ -421,10 +470,27 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering null); } - internal static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId) + public static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId) { _receivedDuplicateBatchAcknowledgement(logger, incomingBatchId, null); } + + public static void FullUnacknowledgedRenderBatchesQueue(ILogger logger) + { + _fullUnacknowledgedRenderBatchesQueue(logger, null); + } } } + + internal readonly struct PendingRender + { + public PendingRender(int componentId, RenderFragment renderFragment) + { + ComponentId = componentId; + RenderFragment = renderFragment; + } + + public int ComponentId { get; } + public RenderFragment RenderFragment { get; } + } } diff --git a/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..2895d6df94 --- /dev/null +++ b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs @@ -0,0 +1,119 @@ +// 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; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Server +{ + /// + /// A base class for services that receive an + /// authentication state from the host environment, and revalidate it at regular intervals. + /// + public abstract class RevalidatingServerAuthenticationStateProvider + : ServerAuthenticationStateProvider, IDisposable + { + private readonly ILogger _logger; + private CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource(); + + /// + /// Constructs an instance of . + /// + /// A logger factory. + public RevalidatingServerAuthenticationStateProvider(ILoggerFactory loggerFactory) + { + if (loggerFactory is null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _logger = loggerFactory.CreateLogger(); + + // Whenever we receive notification of a new authentication state, cancel any + // existing revalidation loop and start a new one + AuthenticationStateChanged += authenticationStateTask => + { + _loopCancellationTokenSource?.Cancel(); + _loopCancellationTokenSource = new CancellationTokenSource(); + _ = RevalidationLoop(authenticationStateTask, _loopCancellationTokenSource.Token); + }; + } + + /// + /// Gets the interval between revalidation attempts. + /// + protected abstract TimeSpan RevalidationInterval { get; } + + /// + /// Determines whether the authentication state is still valid. + /// + /// The current . + /// A to observe while performing the operation. + /// A that resolves as true if the is still valid, or false if it is not. + protected abstract Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken); + + private async Task RevalidationLoop(Task authenticationStateTask, CancellationToken cancellationToken) + { + try + { + var authenticationState = await authenticationStateTask; + if (authenticationState.User.Identity.IsAuthenticated) + { + while (!cancellationToken.IsCancellationRequested) + { + bool isValid; + + try + { + await Task.Delay(RevalidationInterval, cancellationToken); + isValid = await ValidateAuthenticationStateAsync(authenticationState, cancellationToken); + } + catch (TaskCanceledException tce) + { + // If it was our cancellation token, then this revalidation loop gracefully completes + // Otherwise, treat it like any other failure + if (tce.CancellationToken == cancellationToken) + { + break; + } + + throw; + } + + if (!isValid) + { + ForceSignOut(); + break; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while revalidating authentication state"); + ForceSignOut(); + } + } + + private void ForceSignOut() + { + var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); + var anonymousState = new AuthenticationState(anonymousUser); + SetAuthenticationState(Task.FromResult(anonymousState)); + } + + void IDisposable.Dispose() + { + _loopCancellationTokenSource?.Cancel(); + Dispose(disposing: true); + } + + /// + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs index bd1fecfa68..2708b902e9 100644 --- a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs +++ b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs @@ -4,19 +4,21 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Components.Server.Circuits +namespace Microsoft.AspNetCore.Components.Server { /// /// An intended for use in server-side Blazor. /// - internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider + public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider { private Task _authenticationStateTask; + /// public override Task GetAuthenticationStateAsync() => _authenticationStateTask ?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}."); + /// public void SetAuthenticationState(Task authenticationStateTask) { _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask)); diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index 4da9a3391e..f83fb5e210 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -68,34 +68,7 @@ namespace Microsoft.AspNetCore.Components.Server return Task.CompletedTask; } - if (exception != null) - { - return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); - } - else - { - // The client will gracefully disconnect when using websockets by correctly closing the TCP connection. - // This happens when the user closes a tab, navigates away from the page or reloads the page. - // In these situations we know the user is done with the circuit, so we can get rid of it at that point. - // This is important to be able to more efficiently manage resources, specially memory. - return TerminateCircuitGracefully(circuitHost); - } - } - - private async Task TerminateCircuitGracefully(CircuitHost circuitHost) - { - try - { - Log.CircuitTerminatedGracefully(_logger, circuitHost.CircuitId); - _circuitRegistry.PermanentDisconnect(circuitHost); - await circuitHost.DisposeAsync(); - } - catch (Exception e) - { - Log.UnhandledExceptionInCircuit(_logger, circuitHost.CircuitId, e); - } - - await _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); + return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); } /// @@ -242,7 +215,7 @@ namespace Microsoft.AspNetCore.Components.Server } Log.ReceivedConfirmationForBatch(_logger, renderId); - CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull); + _ = CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull); } public void OnLocationChanged(string uri, bool intercepted) diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 3f87bd43af..35e5a62198 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -21,6 +21,16 @@ + + + + @@ -53,12 +63,7 @@ - - ..\..\Web.JS\dist\Release\blazor.server.js - ..\..\Web.JS\dist\Debug\blazor.server.js + ..\..\Web.JS\dist\$(Configuration)\blazor.server.js diff --git a/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs b/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs new file mode 100644 index 0000000000..627c51b576 --- /dev/null +++ b/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs @@ -0,0 +1,244 @@ +// 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.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Server +{ + public class CircuitDisconnectMiddlewareTest + { + [Theory] + [InlineData("GET")] + [InlineData("PUT")] + [InlineData("DELETE")] + [InlineData("HEAD")] + public async Task DisconnectMiddleware_OnlyAccepts_PostRequests(string httpMethod) + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status405MethodNotAllowed, context.Response.StatusCode); + } + + [Theory] + [InlineData(null)] + [InlineData("application/json")] + public async Task Returns400BadRequest_ForInvalidContentTypes(string contentType) + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = contentType; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns400BadRequest_IfNoCircuitIdOnForm() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns400BadRequest_InvalidCircuitId() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = "1234" }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns200OK_NonExistingCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + + [Fact] + public async Task GracefullyTerminates_ConnectedCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var testCircuitHost = TestCircuitHost.Create(id); + + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + registry.Register(testCircuitHost); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + + [Fact] + public async Task GracefullyTerminates_DisconnectedCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var circuitHost = TestCircuitHost.Create(id); + + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + registry.Register(circuitHost); + await registry.DisconnectAsync(circuitHost, "1234"); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + } +} diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 58803a56b2..a6c78f8683 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -240,7 +240,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private class TestRemoteRenderer : RemoteRenderer { public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client) - : base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance) + : base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance) { } diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index ffb913544a..b6475ba1fe 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -8,9 +8,11 @@ using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Moq; @@ -48,7 +50,82 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering component.TriggerRender(); // Assert - Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count); + Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); + } + + [Fact] + public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (int i = 0; i < 20; i++) + { + component.TriggerRender(); + + } + + // Assert + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); + } + + [Fact] + public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (var i = 0; i < 10; i++) + { + component.TriggerRender(); + } + + await renderer.OnRenderCompleted(2, null); + + // Assert + Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count); + } + + + [Fact] + public async Task ProducesNewBatch_WhenABatchGetsAcknowledged() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var i = 0; + var component = new TestComponent(builder => + { + builder.AddContent(0, $"Value {i}"); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (i = 0; i < 20; i++) + { + component.TriggerRender(); + } + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); + + await renderer.OnRenderCompleted(2, null); + + // Assert + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); } [Fact] @@ -83,7 +160,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); @event.Reset(); firstBatchTCS.SetResult(null); @@ -101,7 +178,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering foreach (var id in renderIds.ToArray()) { - renderer.OnRenderCompleted(id, null); + _ = renderer.OnRenderCompleted(id, null); } secondBatchTCS.SetResult(null); @@ -152,7 +229,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); @@ -164,14 +241,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Receive the ack for the intial batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); // Receive the ack for the second batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); // Assert Assert.Empty(exceptions); @@ -215,7 +292,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); @@ -227,14 +304,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Receive the ack for the intial batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); // Receive the ack for the second batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); // Assert Assert.Empty(exceptions); @@ -278,7 +355,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); @@ -288,13 +365,13 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Pretend that we missed the ack for the initial batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Assert Assert.Empty(exceptions); - Assert.Empty(renderer.UnacknowledgedRenderBatches); + Assert.Empty(renderer._unacknowledgedRenderBatches); } [Fact] @@ -335,7 +412,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); @@ -344,7 +421,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering exceptions.Add(e); }; - renderer.OnRenderCompleted(4, null); + _ = renderer.OnRenderCompleted(4, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); @@ -372,7 +449,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // Assert Assert.Equal(0, first.ComponentId); Assert.Equal(1, second.ComponentId); - Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count); + Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); } private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy) @@ -389,6 +466,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering serviceProvider, NullLoggerFactory.Instance, new RendererRegistry(), + new CircuitOptions(), jsRuntime.Object, circuitClientProxy, HtmlEncoder.Default, diff --git a/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs b/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..9f791c41eb --- /dev/null +++ b/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs @@ -0,0 +1,256 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class RevalidatingServerAuthenticationStateProviderTest + { + [Fact] + public void AcceptsAndReturnsAuthStateFromHost() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider(TimeSpan.MaxValue); + + // Act/Assert: Host can supply a value + var hostAuthStateTask = (new TaskCompletionSource()).Task; + provider.SetAuthenticationState(hostAuthStateTask); + Assert.Same(hostAuthStateTask, provider.GetAuthenticationStateAsync()); + + // Act/Assert: Host can supply a changed value + var hostAuthStateTask2 = (new TaskCompletionSource()).Task; + provider.SetAuthenticationState(hostAuthStateTask2); + Assert.Same(hostAuthStateTask2, provider.GetAuthenticationStateAsync()); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsTrue_ContinuesRevalidating() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + var didNotifyAuthenticationStateChanged = false; + provider.AuthenticationStateChanged += _ => { didNotifyAuthenticationStateChanged = true; }; + + // Act + for (var i = 0; i < 10; i++) + { + await provider.NextValidateAuthenticationStateAsyncCall; + } + + // Assert + Assert.Equal(10, provider.RevalidationCallLog.Count); + Assert.False(didNotifyAuthenticationStateChanged); + Assert.Equal("test user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsFalse_ForcesSignOut() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(false); + + var newAuthStateNotificationTcs = new TaskCompletionSource>(); + provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask); + + // Act + var newAuthStateTask = await newAuthStateNotificationTcs.Task; + var newAuthState = await newAuthStateTask; + + // Assert + Assert.False(newAuthState.User.Identity.IsAuthenticated); + + // Assert: no longer revalidates + await Task.Delay(200); + Assert.Single(provider.RevalidationCallLog); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncThrows_ForcesSignOut() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromException(new InvalidTimeZoneException()); + + var newAuthStateNotificationTcs = new TaskCompletionSource>(); + provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask); + + // Act + var newAuthStateTask = await newAuthStateNotificationTcs.Task; + var newAuthState = await newAuthStateTask; + + // Assert + Assert.False(newAuthState.User.Identity.IsAuthenticated); + + // Assert: no longer revalidates + await Task.Delay(200); + Assert.Single(provider.RevalidationCallLog); + } + + [Fact] + public async Task IfHostSuppliesNewAuthenticationState_RestartsRevalidationLoop() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog, + call => Assert.Equal("test user", call.AuthenticationState.User.Identity.Name)); + + // Act/Assert 1: Can become signed out + // Doesn't revalidate unauthenticated states + provider.SetAuthenticationState(CreateAuthenticationStateTask(null)); + await Task.Delay(200); + Assert.Empty(provider.RevalidationCallLog.Skip(1)); + + // Act/Assert 2: Can become a different user; resumes revalidation + provider.SetAuthenticationState(CreateAuthenticationStateTask("different user")); + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog.Skip(1), + call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name)); + } + + [Fact] + public async Task StopsRevalidatingAfterDisposal() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + + // Act + ((IDisposable)provider).Dispose(); + await Task.Delay(200); + + // Assert + Assert.Empty(provider.RevalidationCallLog); + } + + [Fact] + public async Task SuppliesCancellationTokenThatSignalsWhenRevalidationLoopIsBeingDiscarded() + { + // Arrange + var validationTcs = new TaskCompletionSource(); + var authenticationStateChangedCount = 0; + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.NextValidationResult = validationTcs.Task; + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; }; + + // Act/Assert 1: token isn't cancelled initially + await provider.NextValidateAuthenticationStateAsyncCall; + var firstRevalidationCall = provider.RevalidationCallLog.Single(); + Assert.False(firstRevalidationCall.CancellationToken.IsCancellationRequested); + Assert.Equal(0, authenticationStateChangedCount); + + // Have the task throw a TCE to show this doesn't get treated as a failure + firstRevalidationCall.CancellationToken.Register(() => validationTcs.TrySetCanceled(firstRevalidationCall.CancellationToken)); + + // Act/Assert 2: token is cancelled when the loop is superseded + provider.NextValidationResult = Task.FromResult(true); + provider.SetAuthenticationState(CreateAuthenticationStateTask("different user")); + Assert.True(firstRevalidationCall.CancellationToken.IsCancellationRequested); + + // Since we asked for that operation to be cancelled, we don't treat it as a failure and + // don't force a logout + Assert.Equal(1, authenticationStateChangedCount); + Assert.Equal("different user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name); + + // Subsequent revalidation can complete successfully + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog.Skip(1), + call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name)); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsUnrelatedCancelledTask_TreatAsFailure() + { + // Arrange + var validationTcs = new TaskCompletionSource(); + var authenticationStateChangedCount = 0; + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.NextValidationResult = validationTcs.Task; + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; }; + + // Be waiting for the first ValidateAuthenticationStateAsync to complete + await provider.NextValidateAuthenticationStateAsyncCall; + var firstRevalidationCall = provider.RevalidationCallLog.Single(); + Assert.Equal(0, authenticationStateChangedCount); + + // Act: ValidateAuthenticationStateAsync returns cancelled task, but the cancellation + // is unrelated to the CT we supplied + validationTcs.TrySetCanceled(new CancellationTokenSource().Token); + + // Assert: Since we didn't ask for that operation to be cancelled, this is treated as + // a failure to validate, so we force a logout + Assert.Equal(1, authenticationStateChangedCount); + var newAuthState = await provider.GetAuthenticationStateAsync(); + Assert.False(newAuthState.User.Identity.IsAuthenticated); + Assert.Null(newAuthState.User.Identity.Name); + } + + static Task CreateAuthenticationStateTask(string username) + { + var identity = !string.IsNullOrEmpty(username) + ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) }, "testauth") + : new ClaimsIdentity(); + var authenticationState = new AuthenticationState(new ClaimsPrincipal(identity)); + return Task.FromResult(authenticationState); + } + + class TestRevalidatingServerAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider + { + private readonly TimeSpan _revalidationInterval; + private TaskCompletionSource _nextValidateAuthenticationStateAsyncCallSource + = new TaskCompletionSource(); + + public TestRevalidatingServerAuthenticationStateProvider(TimeSpan revalidationInterval) + : base(NullLoggerFactory.Instance) + { + _revalidationInterval = revalidationInterval; + } + + public Task NextValidationResult { get; set; } + + public Task NextValidateAuthenticationStateAsyncCall + => _nextValidateAuthenticationStateAsyncCallSource.Task; + + public List<(AuthenticationState AuthenticationState, CancellationToken CancellationToken)> RevalidationCallLog { get; } + = new List<(AuthenticationState, CancellationToken)>(); + + protected override TimeSpan RevalidationInterval => _revalidationInterval; + + protected override Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken) + { + RevalidationCallLog.Add((authenticationState, cancellationToken)); + var result = NextValidationResult; + var prevCts = _nextValidateAuthenticationStateAsyncCallSource; + _nextValidateAuthenticationStateAsyncCallSource = new TaskCompletionSource(); + prevCts.SetResult(true); + return result; + } + } + } +} diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index f8f5996d95..328120d70c 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits serviceScope.ServiceProvider ?? Mock.Of(), NullLoggerFactory.Instance, new RendererRegistry(), + new CircuitOptions(), jsRuntime, clientProxy, HtmlEncoder.Default, diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index cba13e8581..852ad62f0d 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; @@ -63,6 +64,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests services.AddRouting(); services.AddSignalR(); services.AddServerSideBlazor(); + services.AddSingleton(new ConfigurationBuilder().Build()); var serviceProvder = services.BuildServiceProvider(); diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index d5ce07bf4b..135a5cbda2 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -48,6 +49,9 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers public new int AssignRootComponentId(IComponent component) => base.AssignRootComponentId(component); + public new ArrayRange GetCurrentRenderTreeFrames(int componentId) + => base.GetCurrentRenderTreeFrames(componentId); + public void RenderRootComponent(int componentId, ParameterView? parameters = default) { var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty)); diff --git a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj index f47ada3d4f..8e0a17ece0 100644 --- a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj +++ b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj @@ -7,8 +7,18 @@ - - + + diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index a79de53cec..fc7be4706b 100644 --- a/src/Components/Web.JS/dist/Release/blazor.server.js +++ b/src/Components/Web.JS/dist/Release/blazor.server.js @@ -1,11 +1,11 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=49)}([function(e,t,n){"use strict";var r;n.d(t,"a",function(){return r}),function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(r||(r={}))},function(e,t,n){"use strict";n.d(t,"a",function(){return a}),n.d(t,"c",function(){return c}),n.d(t,"f",function(){return u}),n.d(t,"g",function(){return l}),n.d(t,"h",function(){return f}),n.d(t,"e",function(){return h}),n.d(t,"d",function(){return p}),n.d(t,"b",function(){return d});var r=n(0),o=n(6),i=function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},s=function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!(o=(o=s.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]-1&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),s=n(4),a=n(43),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var a=e.headers;a&&Object.keys(a).forEach(function(e){o.setRequestHeader(e,a[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new s.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(s.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new a.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(s.a),p=n(44);!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new x(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.Debug,"Skipping transport '"+e.transport+"' because it is not supported by this client."),new Error("Skipping transport '"+e.transport+"' because it is not supported by this client.");if(!function(e,t){return!e||0!=(t&e)}(t,r))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.Debug,"HttpConnection.stopConnection("+e+") called while in state "+this.connectionState+"."),this.transport=void 0,e=this.stopError||e,this.stopError=void 0,"Disconnected"!==this.connectionState)if("Connecting "!==this.connectionState){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t)},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(e){if(this.buffer.length&&typeof this.buffer[0]!=typeof e)throw new Error("Expected data to be of type "+typeof this.buffer+" but was of type "+typeof e);this.buffer.push(e),this.sendBufferedData.resolve()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]-1&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),s=n(4),a=n(43),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var a=e.headers;a&&Object.keys(a).forEach(function(e){o.setRequestHeader(e,a[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new s.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(s.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new a.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(s.a),p=n(44);!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new x(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.Debug,"Skipping transport '"+e.transport+"' because it is not supported by this client."),new Error("Skipping transport '"+e.transport+"' because it is not supported by this client.");if(!function(e,t){return!e||0!=(t&e)}(t,r))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.Debug,"HttpConnection.stopConnection("+e+") called while in state "+this.connectionState+"."),this.transport=void 0,e=this.stopError||e,this.stopError=void 0,"Disconnected"!==this.connectionState)if("Connecting "!==this.connectionState){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t)},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(e){if(this.buffer.length&&typeof this.buffer[0]!=typeof e)throw new Error("Expected data to be of type "+typeof this.buffer+" but was of type "+typeof e);this.buffer.push(e),this.sendBufferedData.resolve()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o * @license MIT */ -var r=n(50),o=n(51),i=n(52);function s(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(e,t){if(s()=s())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+s().toString(16)+" bytes");return 0|e}function d(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return F(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return H(e).length;default:if(r)return F(e).length;t=(""+t).toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:v(e,t,n,r,o);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):v(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function v(e,t,n,r,o){var i,s=1,a=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;s=2,a/=2,c/=2,n/=2}function u(e,t){return 1===s?e[t]:e.readUInt16BE(t*s)}if(o){var l=-1;for(i=n;ia&&(n=a-c),i=n;i>=0;i--){for(var f=!0,h=0;ho&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var s=0;s>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function _(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function I(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:u>223?3:u>191?2:1;if(o+f<=n)switch(f){case 1:u<128&&(l=u);break;case 2:128==(192&(i=e[o+1]))&&(c=(31&u)<<6|63&i)>127&&(l=c);break;case 3:i=e[o+1],s=e[o+2],128==(192&i)&&128==(192&s)&&(c=(15&u)<<12|(63&i)<<6|63&s)>2047&&(c<55296||c>57343)&&(l=c);break;case 4:i=e[o+1],s=e[o+2],a=e[o+3],128==(192&i)&&128==(192&s)&&128==(192&a)&&(c=(15&u)<<18|(63&i)<<12|(63&s)<<6|63&a)>65535&&c<1114112&&(l=c)}null===l?(l=65533,f=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=f}return function(e){var t=e.length;if(t<=T)return String.fromCharCode.apply(String,e);var n="",r=0;for(;rthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return x(this,t,n);case"utf8":case"utf-8":return I(this,t,n);case"ascii":return k(this,t,n);case"latin1":case"binary":return P(this,t,n);case"base64":return _(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},c.prototype.compare=function(e,t,n,r,o){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),s=(n>>>=0)-(t>>>=0),a=Math.min(i,s),u=this.slice(r,o),l=e.slice(t,n),f=0;fo)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return m(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return E(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var T=4096;function k(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function O(e,t,n,r,o,i){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function M(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function L(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function A(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function B(e,t,n,r,i){return i||A(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,i){return i||A(e,0,n,8),o.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},c.prototype.readUInt8=function(e,t){return t||D(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||D(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||D(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||D(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||D(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},c.prototype.readInt8=function(e,t){return t||D(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||D(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||D(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||D(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||D(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||O(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):L(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):L(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=0,s=1,a=0;for(this[t]=255&e;++i>0)-a&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=n-1,s=1,a=0;for(this[t+i]=255&e;--i>=0&&(s*=256);)e<0&&0===a&&0!==this[t+i+1]&&(a=1),this[t+i]=(e/s>>0)-a&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):L(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):L(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return B(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return B(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!c.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(s+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function H(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(U,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function q(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(10))},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}()},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.write=function(t){return""+t+e.RecordSeparator},e.parse=function(t){if(t[t.length-1]!==e.RecordSeparator)throw new Error("Message is incomplete.");var n=t.split(e.RecordSeparator);return n.pop(),n},e.RecordSeparatorCode=30,e.RecordSeparator=String.fromCharCode(e.RecordSeparatorCode),e}()},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(9);var r=n(26),o=n(16),i={},s=!1;function a(e,t,n){var o=i[e];o||(o=i[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=a,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(t);if(!r)throw new Error("Could not find any element matching selector '"+t+"'.");a(e,o.toLogicalElement(r,!0),n)},t.renderBatch=function(e,t){var n=i[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),a=r.values(o),c=r.count(o),u=t.referenceFrames(),l=r.values(u),f=t.diffReader,h=0;h=0,"must have a non-negative type"),o(s,"must have a decode function"),this.registerEncoder(function(e){return e instanceof t},function(t){var o=i(),s=r.allocUnsafe(1);return s.writeInt8(e,0),o.append(s),o.append(n(t)),o}),this.registerDecoder(e,s),this},registerEncoder:function(e,n){return o(e,"must have an encode function"),o(n,"must have an encode function"),t.push({check:e,encode:n}),this},registerDecoder:function(e,t){return o(e>=0,"must have a non-negative type"),o(t,"must have a decode function"),n.push({type:e,decode:t}),this},encoder:s.encoder,decoder:s.decoder,buffer:!0,type:"msgpack5",IncompleteBufferError:a.IncompleteBufferError}}},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,i=null;function s(e){t.push(e)}function a(e,t){for(var n=[],r=2;r0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function a(e,t,n){var i=e;if(e instanceof Comment&&(u(i)&&u(i).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(c(i))throw new Error("Not implemented: moving existing logical children");var s=u(t);if(n0;)e(r,0);var i=r;i.parentNode.removeChild(i)},t.getLogicalParent=c,t.getLogicalSiblingEnd=function(e){return e[i]||null},t.getLogicalChild=function(e,t){return u(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=u,t.permuteLogicalChildren=function(e,t){var n=u(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=c(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):h(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,i=r;i;){var s=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=s}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function a(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:s}catch(e){r=s}}();var c,u=[],l=!1,f=-1;function h(){l&&c&&(l=!1,c.length?u=c.concat(u):f=-1,u.length&&p())}function p(){if(!l){var e=a(h);l=!0;for(var t=u.length;t;){for(c=u,u=[];++f1)for(var n=1;n0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]this.length)&&(r=this.length),n>=this.length)return e||i.alloc(0);if(r<=0)return e||i.alloc(0);var o,s,a=!!e,c=this._offset(n),u=r-n,l=u,f=a&&t||0,h=c[1];if(0===n&&r==this.length){if(!a)return 1===this._bufs.length?this._bufs[0]:i.concat(this._bufs,this.length);for(s=0;s(o=this._bufs[s].length-h))){this._bufs[s].copy(e,f,h,h+l);break}this._bufs[s].copy(e,f,h),f+=o,l-=o,h&&(h=0)}return e},s.prototype.shallowSlice=function(e,t){e=e||0,t=t||this.length,e<0&&(e+=this.length),t<0&&(t+=this.length);var n=this._offset(e),r=this._offset(t),o=this._bufs.slice(n[0],r[0]+1);return 0==r[1]?o.pop():o[o.length-1]=o[o.length-1].slice(0,r[1]),0!=n[1]&&(o[0]=o[0].slice(n[1])),new s(o)},s.prototype.toString=function(e,t,n){return this.slice(t,n).toString(e)},s.prototype.consume=function(e){for(;this._bufs.length;){if(!(e>=this._bufs[0].length)){this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift()}return this},s.prototype.duplicate=function(){for(var e=0,t=new s;e0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]=i)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),d(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),l(r,e,r.depth)}function c(e,t){var n=a.styles[t];return n?"["+a.colors[n][0]+"m"+e+"["+a.colors[n][1]+"m":e}function u(e,t){return e}function l(e,n,r){if(e.customInspect&&n&&C(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var o=n.inspect(r,e);return v(o)||(o=l(e,o,r)),o}var i=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(v(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(y(t))return e.stylize(""+t,"number");if(d(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,n);if(i)return i;var s=Object.keys(n),a=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(s);if(e.showHidden&&(s=Object.getOwnPropertyNames(n)),S(n)&&(s.indexOf("message")>=0||s.indexOf("description")>=0))return f(n);if(0===s.length){if(C(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(m(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(E(n))return e.stylize(Date.prototype.toString.call(n),"date");if(S(n))return f(n)}var u,w="",_=!1,I=["{","}"];(p(n)&&(_=!0,I=["[","]"]),C(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return m(n)&&(w=" "+RegExp.prototype.toString.call(n)),E(n)&&(w=" "+Date.prototype.toUTCString.call(n)),S(n)&&(w=" "+f(n)),0!==s.length||_&&0!=n.length?r<0?m(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),u=_?function(e,t,n,r,o){for(var i=[],s=0,a=t.length;s=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(u,w,I)):I[0]+w+I[1]}function f(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,n,r,o,i){var s,a,c;if((c=Object.getOwnPropertyDescriptor(t,o)||{value:t[o]}).get?a=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(a=e.stylize("[Setter]","special")),k(r,o)||(s="["+o+"]"),a||(e.seen.indexOf(c.value)<0?(a=g(n)?l(e,c.value,null):l(e,c.value,n-1)).indexOf("\n")>-1&&(a=i?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),b(s)){if(i&&o.match(/^\d+$/))return a;(s=JSON.stringify(""+o)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(s=s.substr(1,s.length-2),s=e.stylize(s,"name")):(s=s.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),s=e.stylize(s,"string"))}return s+": "+a}function p(e){return Array.isArray(e)}function d(e){return"boolean"==typeof e}function g(e){return null===e}function y(e){return"number"==typeof e}function v(e){return"string"==typeof e}function b(e){return void 0===e}function m(e){return w(e)&&"[object RegExp]"===_(e)}function w(e){return"object"==typeof e&&null!==e}function E(e){return w(e)&&"[object Date]"===_(e)}function S(e){return w(e)&&("[object Error]"===_(e)||e instanceof Error)}function C(e){return"function"==typeof e}function _(e){return Object.prototype.toString.call(e)}function I(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(i)&&(i=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!s[n])if(new RegExp("\\b"+n+"\\b","i").test(i)){var r=e.pid;s[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else s[n]=function(){};return s[n]},t.inspect=a,a.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},a.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=d,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=y,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=m,t.isObject=w,t.isDate=E,t.isError=S,t.isFunction=C,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(54);var T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function k(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(e=new Date,n=[I(e.getHours()),I(e.getMinutes()),I(e.getSeconds())].join(":"),[e.getDate(),T[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(55),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function x(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),o=[],i=0;i0?("string"==typeof t||s.objectMode||Object.getPrototypeOf(t)===u.prototype||(t=function(e){return u.from(e)}(t)),r?s.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):E(e,s,t,!0):s.ended?e.emit("error",new Error("stream.push() after EOF")):(s.reading=!1,s.decoder&&!n?(t=s.decoder.write(t),s.objectMode||0!==t.length?E(e,s,t,!1):T(e,s)):E(e,s,t,!1))):r||(s.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function(e){return e>=S?e=S:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function _(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(p("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?o.nextTick(I,e):I(e))}function I(e){p("emit readable"),e.emit("readable"),R(e)}function T(e,t){t.readingMore||(t.readingMore=!0,o.nextTick(k,e,t))}function k(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;ei.length?i.length:e;if(s===i.length?o+=i:o+=i.slice(0,e),0===(e-=s)){s===i.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=i.slice(s));break}++r}return t.length-=r,o}(e,t):function(e,t){var n=u.allocUnsafe(e),r=t.head,o=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var i=r.data,s=e>i.length?i.length:e;if(i.copy(n,n.length-e,0,s),0===(e-=s)){s===i.length?(++o,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=i.slice(s));break}++o}return t.length-=o,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function O(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,o.nextTick(M,t,e))}function M(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function L(e,t){for(var n=0,r=e.length;n=t.highWaterMark||t.ended))return p("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?O(this):_(this),null;if(0===(e=C(e,t))&&t.ended)return 0===t.length&&O(this),null;var r,o=t.needReadable;return p("need readable",o),(0===t.length||t.length-e0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&O(this)),null!==r&&this.emit("data",r),r},m.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},m.prototype.pipe=function(e,t){var n=this,i=this._readableState;switch(i.pipesCount){case 0:i.pipes=e;break;case 1:i.pipes=[i.pipes,e];break;default:i.pipes.push(e)}i.pipesCount+=1,p("pipe count=%d opts=%j",i.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?l:m;function u(t,r){p("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,p("cleanup"),e.removeListener("close",v),e.removeListener("finish",b),e.removeListener("drain",f),e.removeListener("error",y),e.removeListener("unpipe",u),n.removeListener("end",l),n.removeListener("end",m),n.removeListener("data",g),h=!0,!i.awaitDrain||e._writableState&&!e._writableState.needDrain||f())}function l(){p("onend"),e.end()}i.endEmitted?o.nextTick(c):n.once("end",c),e.on("unpipe",u);var f=function(e){return function(){var t=e._readableState;p("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&a(e,"data")&&(t.flowing=!0,R(e))}}(n);e.on("drain",f);var h=!1;var d=!1;function g(t){p("ondata"),d=!1,!1!==e.write(t)||d||((1===i.pipesCount&&i.pipes===e||i.pipesCount>1&&-1!==L(i.pipes,e))&&!h&&(p("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,d=!0),n.pause())}function y(t){p("onerror",t),m(),e.removeListener("error",y),0===a(e,"error")&&e.emit("error",t)}function v(){e.removeListener("finish",b),m()}function b(){p("onfinish"),e.removeListener("close",v),m()}function m(){p("unpipe"),n.unpipe(e)}return n.on("data",g),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?s(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",y),e.once("close",v),e.once("finish",b),e.emit("pipe",n),i.flowing||(p("pipe resume"),n.resume()),e},m.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,o=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var i=0;i0&&s.length>o&&!s.warned){s.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=s.length,a=c,console&&console.warn&&console.warn(a)}return e}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=function(){for(var e=[],t=0;t0&&(s=t[0]),s instanceof Error)throw s;var a=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw a.context=s,a}var c=o[e];if(void 0===c)return!1;if("function"==typeof c)i(c,this,t);else{var u=c.length,l=d(c,u);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){s=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},a.prototype.listeners=function(e){return h(this,e,!0)},a.prototype.rawListeners=function(e){return h(this,e,!1)},a.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},a.prototype.listenerCount=p,a.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){e.exports=n(37).EventEmitter},function(e,t,n){"use strict";var r=n(23);function o(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,i=this._readableState&&this._readableState.destroyed,s=this._writableState&&this._writableState.destroyed;return i||s?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(o,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(o,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";var r=n(61).Buffer,o=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function i(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===o||!o(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=u,t=4;break;case"utf8":this.fillLast=a,t=4;break;case"base64":this.text=l,this.end=f,t=3;break;default:return this.write=h,void(this.end=p)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function s(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function a(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function u(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function l(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function f(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function h(e){return e.toString(this.encoding)}function p(e){return e&&e.length?this.write(e):""}t.StringDecoder=i,i.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n=0)return o>0&&(e.lastNeed=o-1),o;if(--r=0)return o>0&&(e.lastNeed=o-2),o;if(--r=0)return o>0&&(2===o?o=0:e.lastNeed=o-3),o;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},i.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";(function(t,r,o){var i=n(23);function s(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var o=r.callback;t.pendingcb--,o(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var a,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:i.nextTick;b.WritableState=v;var u=n(19);u.inherits=n(14);var l={deprecate:n(64)},f=n(38),h=n(13).Buffer,p=o.Uint8Array||function(){};var d,g=n(39);function y(){}function v(e,t){a=a||n(11),e=e||{};var r=t instanceof a;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var o=e.highWaterMark,u=e.writableHighWaterMark,l=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(u||0===u)?u:l,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var f=!1===e.decodeStrings;this.decodeStrings=!f,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,o=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,o){--t.pendingcb,n?(i.nextTick(o,r),i.nextTick(_,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(o(r),e._writableState.errorEmitted=!0,e.emit("error",r),_(e,t))}(e,n,r,t,o);else{var s=S(n);s||n.corked||n.bufferProcessing||!n.bufferedRequest||E(e,n),r?c(w,e,n,s,o):w(e,n,s,o)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new s(this)}function b(e){if(a=a||n(11),!(d.call(b,this)||this instanceof a))return new b(e);this._writableState=new v(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),f.call(this)}function m(e,t,n,r,o,i,s){t.writelen=r,t.writecb=s,t.writing=!0,t.sync=!0,n?e._writev(o,t.onwrite):e._write(o,i,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),_(e,t)}function E(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,o=new Array(r),i=t.corkedRequestsFree;i.entry=n;for(var a=0,c=!0;n;)o[a]=n,n.isBuf||(c=!1),n=n.next,a+=1;o.allBuffers=c,m(e,t,!0,t.length,o,"",i.finish),t.pendingcb++,t.lastBufferedRequest=null,i.next?(t.corkedRequestsFree=i.next,i.next=null):t.corkedRequestsFree=new s(t),t.bufferedRequestCount=0}else{for(;n;){var u=n.chunk,l=n.encoding,f=n.callback;if(m(e,t,!1,t.objectMode?1:u.length,u,l,f),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function S(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function C(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),_(e,t)})}function _(e,t){var n=S(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,i.nextTick(C,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}u.inherits(b,f),v.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(v.prototype,"buffer",{get:l.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(d=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!d.call(this,e)||this===b&&(e&&e._writableState instanceof v)}})):d=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,o=this._writableState,s=!1,a=!o.objectMode&&(r=e,h.isBuffer(r)||r instanceof p);return a&&!h.isBuffer(e)&&(e=function(e){return h.from(e)}(e)),"function"==typeof t&&(n=t,t=null),a?t="buffer":t||(t=o.defaultEncoding),"function"!=typeof n&&(n=y),o.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),i.nextTick(t,n)}(this,n):(a||function(e,t,n,r){var o=!0,s=!1;return null===n?s=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(s=new TypeError("Invalid non-string/buffer chunk")),s&&(e.emit("error",s),i.nextTick(r,s),o=!1),o}(this,o,e,n))&&(o.pendingcb++,s=function(e,t,n,r,o,i){if(!n){var s=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=h.from(t,n));return t}(t,r,o);r!==s&&(n=!0,o="buffer",r=s)}var a=t.objectMode?1:r.length;t.length+=a;var c=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,_(e,t),n&&(t.finished?i.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=g.destroy,b.prototype._undestroy=g.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(18),n(62).setImmediate,n(10))},function(e,t,n){"use strict";e.exports=s;var r=n(11),o=n(19);function i(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var o=this._readableState;o.reading=!1,(o.needReadable||o.length=200&&c.statusCode<300?r(new s.b(c.statusCode,c.statusMessage||"",u)):o(new i.b(c.statusMessage||"",c.statusCode||0))});t.abortSignal&&(t.abortSignal.onabort=function(){f.abort(),o(new i.a)})})},n.prototype.getCookieString=function(e){return this.cookieJar.getCookieString(e)},n}(s.a)}).call(this,n(5).Buffer)},function(e,t,n){"use strict";(function(e){n.d(t,"a",function(){return i});var r=n(7),o=n(1),i=function(){function t(){}return t.prototype.writeHandshakeRequest=function(e){return r.a.write(JSON.stringify(e))},t.prototype.parseHandshakeResponse=function(t){var n,i;if(Object(o.g)(t)||void 0!==e&&t instanceof e){var s=new Uint8Array(t);if(-1===(c=s.indexOf(r.a.RecordSeparatorCode)))throw new Error("Message is incomplete.");var a=c+1;n=String.fromCharCode.apply(null,s.slice(0,a)),i=s.byteLength>a?s.slice(a).buffer:null}else{var c,u=t;if(-1===(c=u.indexOf(r.a.RecordSeparator)))throw new Error("Message is incomplete.");a=c+1;n=u.substring(0,a),i=u.length>a?u.substring(a):null}var l=r.a.parse(n),f=JSON.parse(l[0]);if(f.type)throw new Error("Expected a handshake response from the server.");return[i,f]},t}()}).call(this,n(5).Buffer)},,,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!(o=(o=s.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0)&&!(r=i.next()).done;)s.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return s},s=this&&this.__spread||function(){for(var e=[],t=0;t0?r-4:r,f=0;f>16&255,a[c++]=t>>8&255,a[c++]=255&t;2===s&&(t=o[e.charCodeAt(f)]<<2|o[e.charCodeAt(f+1)]>>4,a[c++]=255&t);1===s&&(t=o[e.charCodeAt(f)]<<10|o[e.charCodeAt(f+1)]<<4|o[e.charCodeAt(f+2)]>>2,a[c++]=t>>8&255,a[c++]=255&t);return a},t.fromByteArray=function(e){for(var t,n=e.length,o=n%3,i=[],s=0,a=n-o;sa?a:s+16383));1===o?(t=e[n-1],i.push(r[t>>2]+r[t<<4&63]+"==")):2===o&&(t=(e[n-2]<<8)+e[n-1],i.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return i.join("")};for(var r=[],o=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0,c=s.length;a0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function l(e,t,n){for(var o,i,s=[],a=t;a>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return s.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,o){var i,s,a=8*o-r-1,c=(1<>1,l=-7,f=n?o-1:0,h=n?-1:1,p=e[t+f];for(f+=h,i=p&(1<<-l)-1,p>>=-l,l+=a;l>0;i=256*i+e[t+f],f+=h,l-=8);for(s=i&(1<<-l)-1,i>>=-l,l+=r;l>0;s=256*s+e[t+f],f+=h,l-=8);if(0===i)i=1-u;else{if(i===c)return s?NaN:1/0*(p?-1:1);s+=Math.pow(2,r),i-=u}return(p?-1:1)*s*Math.pow(2,i-r)},t.write=function(e,t,n,r,o,i){var s,a,c,u=8*i-o-1,l=(1<>1,h=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:i-1,d=r?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=l):(s=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-s))<1&&(s--,c*=2),(t+=s+f>=1?h/c:h*Math.pow(2,1-f))*c>=2&&(s++,c/=2),s+f>=l?(a=0,s=l):s+f>=1?(a=(t*c-1)*Math.pow(2,o),s+=f):(a=t*Math.pow(2,f-1)*Math.pow(2,o),s=0));o>=8;e[n+p]=255&a,p+=d,a/=256,o-=8);for(s=s<0;e[n+p]=255&s,p+=d,s/=256,u-=8);e[n+p-d]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";(function(t){ +var r=n(50),o=n(51),i=n(52);function s(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(e,t){if(s()=s())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+s().toString(16)+" bytes");return 0|e}function d(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return F(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return H(e).length;default:if(r)return F(e).length;t=(""+t).toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:v(e,t,n,r,o);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):v(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function v(e,t,n,r,o){var i,s=1,a=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;s=2,a/=2,c/=2,n/=2}function u(e,t){return 1===s?e[t]:e.readUInt16BE(t*s)}if(o){var l=-1;for(i=n;ia&&(n=a-c),i=n;i>=0;i--){for(var f=!0,h=0;ho&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var s=0;s>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function _(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function I(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:u>223?3:u>191?2:1;if(o+f<=n)switch(f){case 1:u<128&&(l=u);break;case 2:128==(192&(i=e[o+1]))&&(c=(31&u)<<6|63&i)>127&&(l=c);break;case 3:i=e[o+1],s=e[o+2],128==(192&i)&&128==(192&s)&&(c=(15&u)<<12|(63&i)<<6|63&s)>2047&&(c<55296||c>57343)&&(l=c);break;case 4:i=e[o+1],s=e[o+2],a=e[o+3],128==(192&i)&&128==(192&s)&&128==(192&a)&&(c=(15&u)<<18|(63&i)<<12|(63&s)<<6|63&a)>65535&&c<1114112&&(l=c)}null===l?(l=65533,f=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=f}return function(e){var t=e.length;if(t<=T)return String.fromCharCode.apply(String,e);var n="",r=0;for(;rthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return x(this,t,n);case"utf8":case"utf-8":return I(this,t,n);case"ascii":return k(this,t,n);case"latin1":case"binary":return P(this,t,n);case"base64":return _(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},c.prototype.compare=function(e,t,n,r,o){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),s=(n>>>=0)-(t>>>=0),a=Math.min(i,s),u=this.slice(r,o),l=e.slice(t,n),f=0;fo)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return m(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return E(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var T=4096;function k(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function O(e,t,n,r,o,i){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function L(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function M(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function A(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function B(e,t,n,r,i){return i||A(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,i){return i||A(e,0,n,8),o.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},c.prototype.readUInt8=function(e,t){return t||D(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||D(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||D(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||D(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||D(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},c.prototype.readInt8=function(e,t){return t||D(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||D(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||D(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||D(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||D(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||O(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):L(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):L(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):M(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=0,s=1,a=0;for(this[t]=255&e;++i>0)-a&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=n-1,s=1,a=0;for(this[t+i]=255&e;--i>=0&&(s*=256);)e<0&&0===a&&0!==this[t+i+1]&&(a=1),this[t+i]=(e/s>>0)-a&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):L(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):L(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):M(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return B(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return B(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!c.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(s+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function H(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(U,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function q(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(10))},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}()},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.write=function(t){return""+t+e.RecordSeparator},e.parse=function(t){if(t[t.length-1]!==e.RecordSeparator)throw new Error("Message is incomplete.");var n=t.split(e.RecordSeparator);return n.pop(),n},e.RecordSeparatorCode=30,e.RecordSeparator=String.fromCharCode(e.RecordSeparatorCode),e}()},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(9);var r=n(26),o=n(16),i={},s=!1;function a(e,t,n){var o=i[e];o||(o=i[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=a,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(t);if(!r)throw new Error("Could not find any element matching selector '"+t+"'.");a(e,o.toLogicalElement(r,!0),n)},t.renderBatch=function(e,t){var n=i[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),a=r.values(o),c=r.count(o),u=t.referenceFrames(),l=r.values(u),f=t.diffReader,h=0;h=0,"must have a non-negative type"),o(s,"must have a decode function"),this.registerEncoder(function(e){return e instanceof t},function(t){var o=i(),s=r.allocUnsafe(1);return s.writeInt8(e,0),o.append(s),o.append(n(t)),o}),this.registerDecoder(e,s),this},registerEncoder:function(e,n){return o(e,"must have an encode function"),o(n,"must have an encode function"),t.push({check:e,encode:n}),this},registerDecoder:function(e,t){return o(e>=0,"must have a non-negative type"),o(t,"must have a decode function"),n.push({type:e,decode:t}),this},encoder:s.encoder,decoder:s.decoder,buffer:!0,type:"msgpack5",IncompleteBufferError:a.IncompleteBufferError}}},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,i=null;function s(e){t.push(e)}function a(e,t){for(var n=[],r=2;r0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function a(e,t,n){var i=e;if(e instanceof Comment&&(u(i)&&u(i).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(c(i))throw new Error("Not implemented: moving existing logical children");var s=u(t);if(n0;)e(r,0);var i=r;i.parentNode.removeChild(i)},t.getLogicalParent=c,t.getLogicalSiblingEnd=function(e){return e[i]||null},t.getLogicalChild=function(e,t){return u(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=u,t.permuteLogicalChildren=function(e,t){var n=u(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=c(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):h(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,i=r;i;){var s=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=s}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function a(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:s}catch(e){r=s}}();var c,u=[],l=!1,f=-1;function h(){l&&c&&(l=!1,c.length?u=c.concat(u):f=-1,u.length&&p())}function p(){if(!l){var e=a(h);l=!0;for(var t=u.length;t;){for(c=u,u=[];++f1)for(var n=1;n0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]this.length)&&(r=this.length),n>=this.length)return e||i.alloc(0);if(r<=0)return e||i.alloc(0);var o,s,a=!!e,c=this._offset(n),u=r-n,l=u,f=a&&t||0,h=c[1];if(0===n&&r==this.length){if(!a)return 1===this._bufs.length?this._bufs[0]:i.concat(this._bufs,this.length);for(s=0;s(o=this._bufs[s].length-h))){this._bufs[s].copy(e,f,h,h+l);break}this._bufs[s].copy(e,f,h),f+=o,l-=o,h&&(h=0)}return e},s.prototype.shallowSlice=function(e,t){e=e||0,t=t||this.length,e<0&&(e+=this.length),t<0&&(t+=this.length);var n=this._offset(e),r=this._offset(t),o=this._bufs.slice(n[0],r[0]+1);return 0==r[1]?o.pop():o[o.length-1]=o[o.length-1].slice(0,r[1]),0!=n[1]&&(o[0]=o[0].slice(n[1])),new s(o)},s.prototype.toString=function(e,t,n){return this.slice(t,n).toString(e)},s.prototype.consume=function(e){for(;this._bufs.length;){if(!(e>=this._bufs[0].length)){this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift()}return this},s.prototype.duplicate=function(){for(var e=0,t=new s;e0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]=i)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),d(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),l(r,e,r.depth)}function c(e,t){var n=a.styles[t];return n?"["+a.colors[n][0]+"m"+e+"["+a.colors[n][1]+"m":e}function u(e,t){return e}function l(e,n,r){if(e.customInspect&&n&&C(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var o=n.inspect(r,e);return v(o)||(o=l(e,o,r)),o}var i=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(v(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(y(t))return e.stylize(""+t,"number");if(d(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,n);if(i)return i;var s=Object.keys(n),a=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(s);if(e.showHidden&&(s=Object.getOwnPropertyNames(n)),S(n)&&(s.indexOf("message")>=0||s.indexOf("description")>=0))return f(n);if(0===s.length){if(C(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(m(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(E(n))return e.stylize(Date.prototype.toString.call(n),"date");if(S(n))return f(n)}var u,w="",_=!1,I=["{","}"];(p(n)&&(_=!0,I=["[","]"]),C(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return m(n)&&(w=" "+RegExp.prototype.toString.call(n)),E(n)&&(w=" "+Date.prototype.toUTCString.call(n)),S(n)&&(w=" "+f(n)),0!==s.length||_&&0!=n.length?r<0?m(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),u=_?function(e,t,n,r,o){for(var i=[],s=0,a=t.length;s=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(u,w,I)):I[0]+w+I[1]}function f(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,n,r,o,i){var s,a,c;if((c=Object.getOwnPropertyDescriptor(t,o)||{value:t[o]}).get?a=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(a=e.stylize("[Setter]","special")),k(r,o)||(s="["+o+"]"),a||(e.seen.indexOf(c.value)<0?(a=g(n)?l(e,c.value,null):l(e,c.value,n-1)).indexOf("\n")>-1&&(a=i?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),b(s)){if(i&&o.match(/^\d+$/))return a;(s=JSON.stringify(""+o)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(s=s.substr(1,s.length-2),s=e.stylize(s,"name")):(s=s.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),s=e.stylize(s,"string"))}return s+": "+a}function p(e){return Array.isArray(e)}function d(e){return"boolean"==typeof e}function g(e){return null===e}function y(e){return"number"==typeof e}function v(e){return"string"==typeof e}function b(e){return void 0===e}function m(e){return w(e)&&"[object RegExp]"===_(e)}function w(e){return"object"==typeof e&&null!==e}function E(e){return w(e)&&"[object Date]"===_(e)}function S(e){return w(e)&&("[object Error]"===_(e)||e instanceof Error)}function C(e){return"function"==typeof e}function _(e){return Object.prototype.toString.call(e)}function I(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(i)&&(i=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!s[n])if(new RegExp("\\b"+n+"\\b","i").test(i)){var r=e.pid;s[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else s[n]=function(){};return s[n]},t.inspect=a,a.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},a.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=d,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=y,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=m,t.isObject=w,t.isDate=E,t.isError=S,t.isFunction=C,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(54);var T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function k(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(e=new Date,n=[I(e.getHours()),I(e.getMinutes()),I(e.getSeconds())].join(":"),[e.getDate(),T[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(55),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function x(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),o=[],i=0;i0?("string"==typeof t||s.objectMode||Object.getPrototypeOf(t)===u.prototype||(t=function(e){return u.from(e)}(t)),r?s.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):E(e,s,t,!0):s.ended?e.emit("error",new Error("stream.push() after EOF")):(s.reading=!1,s.decoder&&!n?(t=s.decoder.write(t),s.objectMode||0!==t.length?E(e,s,t,!1):T(e,s)):E(e,s,t,!1))):r||(s.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function(e){return e>=S?e=S:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function _(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(p("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?o.nextTick(I,e):I(e))}function I(e){p("emit readable"),e.emit("readable"),R(e)}function T(e,t){t.readingMore||(t.readingMore=!0,o.nextTick(k,e,t))}function k(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;ei.length?i.length:e;if(s===i.length?o+=i:o+=i.slice(0,e),0===(e-=s)){s===i.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=i.slice(s));break}++r}return t.length-=r,o}(e,t):function(e,t){var n=u.allocUnsafe(e),r=t.head,o=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var i=r.data,s=e>i.length?i.length:e;if(i.copy(n,n.length-e,0,s),0===(e-=s)){s===i.length?(++o,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=i.slice(s));break}++o}return t.length-=o,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function O(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,o.nextTick(L,t,e))}function L(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function M(e,t){for(var n=0,r=e.length;n=t.highWaterMark||t.ended))return p("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?O(this):_(this),null;if(0===(e=C(e,t))&&t.ended)return 0===t.length&&O(this),null;var r,o=t.needReadable;return p("need readable",o),(0===t.length||t.length-e0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&O(this)),null!==r&&this.emit("data",r),r},m.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},m.prototype.pipe=function(e,t){var n=this,i=this._readableState;switch(i.pipesCount){case 0:i.pipes=e;break;case 1:i.pipes=[i.pipes,e];break;default:i.pipes.push(e)}i.pipesCount+=1,p("pipe count=%d opts=%j",i.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?l:m;function u(t,r){p("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,p("cleanup"),e.removeListener("close",v),e.removeListener("finish",b),e.removeListener("drain",f),e.removeListener("error",y),e.removeListener("unpipe",u),n.removeListener("end",l),n.removeListener("end",m),n.removeListener("data",g),h=!0,!i.awaitDrain||e._writableState&&!e._writableState.needDrain||f())}function l(){p("onend"),e.end()}i.endEmitted?o.nextTick(c):n.once("end",c),e.on("unpipe",u);var f=function(e){return function(){var t=e._readableState;p("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&a(e,"data")&&(t.flowing=!0,R(e))}}(n);e.on("drain",f);var h=!1;var d=!1;function g(t){p("ondata"),d=!1,!1!==e.write(t)||d||((1===i.pipesCount&&i.pipes===e||i.pipesCount>1&&-1!==M(i.pipes,e))&&!h&&(p("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,d=!0),n.pause())}function y(t){p("onerror",t),m(),e.removeListener("error",y),0===a(e,"error")&&e.emit("error",t)}function v(){e.removeListener("finish",b),m()}function b(){p("onfinish"),e.removeListener("close",v),m()}function m(){p("unpipe"),n.unpipe(e)}return n.on("data",g),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?s(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",y),e.once("close",v),e.once("finish",b),e.emit("pipe",n),i.flowing||(p("pipe resume"),n.resume()),e},m.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,o=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var i=0;i0&&s.length>o&&!s.warned){s.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=s.length,a=c,console&&console.warn&&console.warn(a)}return e}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=function(){for(var e=[],t=0;t0&&(s=t[0]),s instanceof Error)throw s;var a=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw a.context=s,a}var c=o[e];if(void 0===c)return!1;if("function"==typeof c)i(c,this,t);else{var u=c.length,l=d(c,u);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){s=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},a.prototype.listeners=function(e){return h(this,e,!0)},a.prototype.rawListeners=function(e){return h(this,e,!1)},a.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},a.prototype.listenerCount=p,a.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){e.exports=n(37).EventEmitter},function(e,t,n){"use strict";var r=n(23);function o(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,i=this._readableState&&this._readableState.destroyed,s=this._writableState&&this._writableState.destroyed;return i||s?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(o,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(o,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";var r=n(61).Buffer,o=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function i(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===o||!o(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=u,t=4;break;case"utf8":this.fillLast=a,t=4;break;case"base64":this.text=l,this.end=f,t=3;break;default:return this.write=h,void(this.end=p)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function s(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function a(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function u(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function l(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function f(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function h(e){return e.toString(this.encoding)}function p(e){return e&&e.length?this.write(e):""}t.StringDecoder=i,i.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n=0)return o>0&&(e.lastNeed=o-1),o;if(--r=0)return o>0&&(e.lastNeed=o-2),o;if(--r=0)return o>0&&(2===o?o=0:e.lastNeed=o-3),o;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},i.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";(function(t,r,o){var i=n(23);function s(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var o=r.callback;t.pendingcb--,o(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var a,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:i.nextTick;b.WritableState=v;var u=n(19);u.inherits=n(14);var l={deprecate:n(64)},f=n(38),h=n(13).Buffer,p=o.Uint8Array||function(){};var d,g=n(39);function y(){}function v(e,t){a=a||n(11),e=e||{};var r=t instanceof a;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var o=e.highWaterMark,u=e.writableHighWaterMark,l=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(u||0===u)?u:l,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var f=!1===e.decodeStrings;this.decodeStrings=!f,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,o=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,o){--t.pendingcb,n?(i.nextTick(o,r),i.nextTick(_,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(o(r),e._writableState.errorEmitted=!0,e.emit("error",r),_(e,t))}(e,n,r,t,o);else{var s=S(n);s||n.corked||n.bufferProcessing||!n.bufferedRequest||E(e,n),r?c(w,e,n,s,o):w(e,n,s,o)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new s(this)}function b(e){if(a=a||n(11),!(d.call(b,this)||this instanceof a))return new b(e);this._writableState=new v(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),f.call(this)}function m(e,t,n,r,o,i,s){t.writelen=r,t.writecb=s,t.writing=!0,t.sync=!0,n?e._writev(o,t.onwrite):e._write(o,i,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),_(e,t)}function E(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,o=new Array(r),i=t.corkedRequestsFree;i.entry=n;for(var a=0,c=!0;n;)o[a]=n,n.isBuf||(c=!1),n=n.next,a+=1;o.allBuffers=c,m(e,t,!0,t.length,o,"",i.finish),t.pendingcb++,t.lastBufferedRequest=null,i.next?(t.corkedRequestsFree=i.next,i.next=null):t.corkedRequestsFree=new s(t),t.bufferedRequestCount=0}else{for(;n;){var u=n.chunk,l=n.encoding,f=n.callback;if(m(e,t,!1,t.objectMode?1:u.length,u,l,f),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function S(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function C(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),_(e,t)})}function _(e,t){var n=S(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,i.nextTick(C,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}u.inherits(b,f),v.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(v.prototype,"buffer",{get:l.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(d=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!d.call(this,e)||this===b&&(e&&e._writableState instanceof v)}})):d=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,o=this._writableState,s=!1,a=!o.objectMode&&(r=e,h.isBuffer(r)||r instanceof p);return a&&!h.isBuffer(e)&&(e=function(e){return h.from(e)}(e)),"function"==typeof t&&(n=t,t=null),a?t="buffer":t||(t=o.defaultEncoding),"function"!=typeof n&&(n=y),o.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),i.nextTick(t,n)}(this,n):(a||function(e,t,n,r){var o=!0,s=!1;return null===n?s=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(s=new TypeError("Invalid non-string/buffer chunk")),s&&(e.emit("error",s),i.nextTick(r,s),o=!1),o}(this,o,e,n))&&(o.pendingcb++,s=function(e,t,n,r,o,i){if(!n){var s=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=h.from(t,n));return t}(t,r,o);r!==s&&(n=!0,o="buffer",r=s)}var a=t.objectMode?1:r.length;t.length+=a;var c=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,_(e,t),n&&(t.finished?i.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=g.destroy,b.prototype._undestroy=g.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(18),n(62).setImmediate,n(10))},function(e,t,n){"use strict";e.exports=s;var r=n(11),o=n(19);function i(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var o=this._readableState;o.reading=!1,(o.needReadable||o.length=200&&c.statusCode<300?r(new s.b(c.statusCode,c.statusMessage||"",u)):o(new i.b(c.statusMessage||"",c.statusCode||0))});t.abortSignal&&(t.abortSignal.onabort=function(){f.abort(),o(new i.a)})})},n.prototype.getCookieString=function(e){return this.cookieJar.getCookieString(e)},n}(s.a)}).call(this,n(5).Buffer)},function(e,t,n){"use strict";(function(e){n.d(t,"a",function(){return i});var r=n(7),o=n(1),i=function(){function t(){}return t.prototype.writeHandshakeRequest=function(e){return r.a.write(JSON.stringify(e))},t.prototype.parseHandshakeResponse=function(t){var n,i;if(Object(o.g)(t)||void 0!==e&&t instanceof e){var s=new Uint8Array(t);if(-1===(c=s.indexOf(r.a.RecordSeparatorCode)))throw new Error("Message is incomplete.");var a=c+1;n=String.fromCharCode.apply(null,s.slice(0,a)),i=s.byteLength>a?s.slice(a).buffer:null}else{var c,u=t;if(-1===(c=u.indexOf(r.a.RecordSeparator)))throw new Error("Message is incomplete.");a=c+1;n=u.substring(0,a),i=u.length>a?u.substring(a):null}var l=r.a.parse(n),f=JSON.parse(l[0]);if(f.type)throw new Error("Expected a handshake response from the server.");return[i,f]},t}()}).call(this,n(5).Buffer)},,,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!(o=(o=s.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0)&&!(r=i.next()).done;)s.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return s},s=this&&this.__spread||function(){for(var e=[],t=0;t0?r-4:r,f=0;f>16&255,a[c++]=t>>8&255,a[c++]=255&t;2===s&&(t=o[e.charCodeAt(f)]<<2|o[e.charCodeAt(f+1)]>>4,a[c++]=255&t);1===s&&(t=o[e.charCodeAt(f)]<<10|o[e.charCodeAt(f+1)]<<4|o[e.charCodeAt(f+2)]>>2,a[c++]=t>>8&255,a[c++]=255&t);return a},t.fromByteArray=function(e){for(var t,n=e.length,o=n%3,i=[],s=0,a=n-o;sa?a:s+16383));1===o?(t=e[n-1],i.push(r[t>>2]+r[t<<4&63]+"==")):2===o&&(t=(e[n-2]<<8)+e[n-1],i.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return i.join("")};for(var r=[],o=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0,c=s.length;a0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function l(e,t,n){for(var o,i,s=[],a=t;a>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return s.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,o){var i,s,a=8*o-r-1,c=(1<>1,l=-7,f=n?o-1:0,h=n?-1:1,p=e[t+f];for(f+=h,i=p&(1<<-l)-1,p>>=-l,l+=a;l>0;i=256*i+e[t+f],f+=h,l-=8);for(s=i&(1<<-l)-1,i>>=-l,l+=r;l>0;s=256*s+e[t+f],f+=h,l-=8);if(0===i)i=1-u;else{if(i===c)return s?NaN:1/0*(p?-1:1);s+=Math.pow(2,r),i-=u}return(p?-1:1)*s*Math.pow(2,i-r)},t.write=function(e,t,n,r,o,i){var s,a,c,u=8*i-o-1,l=(1<>1,h=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:i-1,d=r?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=l):(s=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-s))<1&&(s--,c*=2),(t+=s+f>=1?h/c:h*Math.pow(2,1-f))*c>=2&&(s++,c/=2),s+f>=l?(a=0,s=l):s+f>=1?(a=(t*c-1)*Math.pow(2,o),s+=f):(a=t*Math.pow(2,f-1)*Math.pow(2,o),s=0));o>=8;e[n+p]=255&a,p+=d,a/=256,o-=8);for(s=s<0;e[n+p]=255&s,p+=d,s/=256,u-=8);e[n+p-d]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";(function(t){ /*! * The buffer module from node.js, for the browser. * diff --git a/src/Components/Web.JS/package.json b/src/Components/Web.JS/package.json index ea59361494..327aaaafea 100644 --- a/src/Components/Web.JS/package.json +++ b/src/Components/Web.JS/package.json @@ -5,9 +5,12 @@ "description": "", "main": "index.js", "scripts": { + "preclean": "yarn install --mutex network", + "clean": "node node_modules/rimraf/bin.js ./dist/Debug ./dist/Release", + "prebuild": "yarn run clean && yarn install --mutex network", "build": "yarn run build:debug && yarn run build:production", - "build:debug": "cd src && webpack --mode development --config ./webpack.config.js", - "build:production": "cd src && webpack --mode production --config ./webpack.config.js", + "build:debug": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode development --config ./webpack.config.js", + "build:production": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode production --config ./webpack.config.js", "test": "jest" }, "devDependencies": { @@ -21,6 +24,7 @@ "@typescript-eslint/parser": "^1.5.0", "eslint": "^5.16.0", "jest": "^24.8.0", + "rimraf": "^2.6.2", "ts-jest": "^24.0.0", "ts-loader": "^4.4.1", "typescript": "^3.5.3", diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts index df96f1031f..e52113d7ac 100644 --- a/src/Components/Web.JS/src/Boot.Server.ts +++ b/src/Components/Web.JS/src/Boot.Server.ts @@ -47,6 +47,16 @@ async function boot(userOptions?: Partial): Promise { return true; }; + window.addEventListener( + 'unload', + () => { + const data = new FormData(); + data.set('circuitId', circuit.circuitId); + navigator.sendBeacon('_blazor/disconnect', data); + }, + false + ); + window['Blazor'].reconnect = reconnect; logger.log(LogLevel.Information, 'Blazor server-side application started.'); diff --git a/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts b/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts index 62f207415e..d59e0fecfe 100644 --- a/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts +++ b/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts @@ -58,23 +58,24 @@ describe('DefaultReconnectionHandler', () => { expect(reconnect).toHaveBeenCalledTimes(1); }); - it('invokes failed if reconnect fails', async () => { - const testDisplay = createTestDisplay(); - const reconnect = jest.fn().mockRejectedValue(null); - const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect); - window.console.error = jest.fn(); + // Skipped while under investigation: https://github.com/aspnet/AspNetCore/issues/12578 + // it('invokes failed if reconnect fails', async () => { + // const testDisplay = createTestDisplay(); + // const reconnect = jest.fn().mockRejectedValue(null); + // const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect); + // window.console.error = jest.fn(); - handler.onConnectionDown({ - maxRetries: 3, - retryIntervalMilliseconds: 20, - dialogId: 'ignored' - }); + // handler.onConnectionDown({ + // maxRetries: 3, + // retryIntervalMilliseconds: 20, + // dialogId: 'ignored' + // }); - await delay(100); - expect(testDisplay.show).toHaveBeenCalled(); - expect(testDisplay.failed).toHaveBeenCalled(); - expect(reconnect).toHaveBeenCalledTimes(3); - }); + // await delay(500); + // expect(testDisplay.show).toHaveBeenCalled(); + // expect(testDisplay.failed).toHaveBeenCalled(); + // expect(reconnect).toHaveBeenCalledTimes(3); + // }); }); function attachUserSpecifiedUI(options: ReconnectionOptions): Element { diff --git a/src/Components/Web.JS/yarn.lock b/src/Components/Web.JS/yarn.lock index f49df8df7d..48b8058558 100644 --- a/src/Components/Web.JS/yarn.lock +++ b/src/Components/Web.JS/yarn.lock @@ -2141,7 +2141,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@^7.1.1, glob@^7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -2153,6 +2153,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -2402,16 +2414,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" diff --git a/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs index f383818386..188fae57d0 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task ReloadingThePage_GracefullyDisconnects_TheCurrentCircuit() { // Arrange & Act - _ = ((IJavaScriptExecutor)Browser).ExecuteScript("location.reload()"); + Browser.Navigate().Refresh(); await Task.WhenAny(Task.Delay(10000), GracefulDisconnectCompletionSource.Task); // Assert @@ -70,7 +70,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Browser.Close(); // Set to null so that other tests in this class can create a new browser if necessary so // that tests don't fail when running together. - Browser = null; await Task.WhenAny(Task.Delay(10000), GracefulDisconnectCompletionSource.Task); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs index 8b46ee7e0a..c3ad07d7e4 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs @@ -10,6 +10,8 @@ using Ignitor; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -17,9 +19,10 @@ using Xunit; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { + [Flaky("https://github.com/aspnet/AspNetCore/issues/12940", FlakyOn.All)] public class InteropReliabilityTests : IClassFixture { - private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(5); private readonly AspNetSiteServerFixture _serverFixture; public InteropReliabilityTests(AspNetSiteServerFixture serverFixture) @@ -256,7 +259,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests await ValidateClientKeepsWorking(Client, batches); } - [Fact] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12962")] public async Task LogsJSInteropCompletionsCallbacksAndContinuesWorkingInAllSituations() { // Arrange @@ -507,15 +510,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception)); // Act - await Client.ClickAsync("event-handler-throw-sync"); + await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); Assert.Contains( logEvents, e => LogLevel.Warning == e.logLevel && "UnhandledExceptionInCircuit" == e.eventIdName && "Handler threw an exception" == e.exception.Message); - - await ValidateClientKeepsWorking(Client, batches); } private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) => diff --git a/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs new file mode 100644 index 0000000000..7377b28a9d --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs @@ -0,0 +1,124 @@ +// 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; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Ignitor; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class RemoteRendererBufferLimitTest : IClassFixture, IDisposable + { + private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500); + + private AspNetSiteServerFixture _serverFixture; + + public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture) + { + serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; + _serverFixture = serverFixture; + + // Needed here for side-effects + _ = _serverFixture.RootUri; + + Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; + Client.RenderBatchReceived += (rendererId, id, data) => Batches.Add(new Batch(rendererId, id, data)); + + Sink = _serverFixture.Host.Services.GetRequiredService(); + Sink.MessageLogged += LogMessages; + } + + public BlazorClient Client { get; set; } + + private IList Batches { get; set; } = new List(); + + // We use a stack so that we can search the logs in reverse order + private ConcurrentStack Logs { get; set; } = new ConcurrentStack(); + + public TestSink Sink { get; private set; } + + [Fact] + public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged() + { + // Arrange + var baseUri = new Uri(_serverFixture.RootUri, "/subdir"); + Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app"); + Assert.Single(Batches); + + await Client.SelectAsync("test-selector-select", "BasicTestApp.LimitCounterComponent"); + Client.ConfirmRenderBatch = false; + + for (int i = 0; i < 10; i++) + { + await Client.ClickAsync("increment"); + } + await Client.ClickAsync("increment", expectRenderBatch: false); + + Assert.Single(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message)); + Assert.Equal("10", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); + var fullCount = Batches.Count; + + // Act + await Client.ClickAsync("increment", expectRenderBatch: false); + + Assert.Contains(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message)); + Assert.Equal(fullCount, Batches.Count); + Client.ConfirmRenderBatch = true; + + // This will resume the render batches. + await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id)); + + // Assert + Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); + Assert.Equal(fullCount + 1, Batches.Count); + } + + private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception)); + + [DebuggerDisplay("{Message,nq}")] + private class LogMessage + { + public LogMessage(LogLevel logLevel, string message, Exception exception) + { + LogLevel = logLevel; + Message = message; + Exception = exception; + } + + public LogLevel LogLevel { get; set; } + public string Message { get; set; } + public Exception Exception { get; set; } + } + + private class Batch + { + public Batch(int rendererId, int id, byte[] data) + { + Id = id; + RendererId = rendererId; + Data = data; + } + + public int Id { get; } + public int RendererId { get; } + public byte[] Data { get; } + } + + public void Dispose() + { + if (Sink != null) + { + Sink.MessageLogged -= LogMessages; + } + } + } +} diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index deaa6bfb64..655505078a 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -66,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized")); Browser.Equal("You're not authorized, anonymous", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -75,6 +78,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -84,6 +88,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -93,6 +98,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Some User", () => appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -102,6 +108,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Bert!", () => appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -111,6 +118,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Mallory", () => appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -120,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -129,6 +138,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -138,6 +148,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Welcome to PageRequiringAuthorization!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -147,6 +158,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Sorry, anonymous, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -156,6 +168,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Welcome to PageRequiringPolicy!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -165,6 +178,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Sorry, Mallory, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -174,6 +188,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Welcome to PageRequiringRole!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -183,6 +198,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Sorry, Bert, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); + } + + private void AssertExpectedLayoutUsed() + { + WaitUntilExists(By.Id("auth-links")); } protected IWebElement MountAndNavigateToAuthTest(string authLinkText) diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor index 5ec7bbe529..917111085a 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -8,19 +8,21 @@ and @page authorization rules. *@ - - - Authorizing... - -
- Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. -
-
-
-
- -
- + + + + Authorizing... + +
+ Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. +
+
+
+
+ +

There's nothing here

+
+
@code { protected override void OnInitialized() diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor similarity index 93% rename from src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor rename to src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor index 35ff333ed7..71c040058f 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor @@ -1,3 +1,8 @@ +@inherits LayoutComponentBase + +@Body + +