diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index e356ab3d42..af04da8a81 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -71,6 +71,7 @@ variables: # The following extra properties are not set when testing. Use with final build.[cmd,sh] of asset-producing jobs. - name: _PublishArgs value: /p:Publish=true + /p:GenerateChecksums=true /p:DotNetPublishBlobFeedKey=$(dotnetfeed-storage-access-key-1) /p:DotNetPublishBlobFeedUrl=https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json /p:DotNetPublishToBlobFeed=$(_DotNetPublishToBlobFeed) @@ -92,27 +93,28 @@ stages: displayName: Build jobs: # Code check - - template: jobs/default-build.yml - parameters: - jobName: Code_check - jobDisplayName: Code check - agentOs: Windows - steps: - - ${{ if ne(variables['System.TeamProject'], 'public') }}: - - task: PowerShell@2 - displayName: Setup Private Feeds Credentials - inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 - arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token - env: - Token: $(dn-bot-dnceng-artifact-feeds-rw) - - powershell: ./eng/scripts/CodeCheck.ps1 -ci $(_InternalRuntimeDownloadArgs) - displayName: Run eng/scripts/CodeCheck.ps1 - artifacts: - - name: Code_Check_Logs - path: artifacts/log/ - publishOnError: true - includeForks: true + - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}: + - template: jobs/default-build.yml + parameters: + jobName: Code_check + jobDisplayName: Code check + agentOs: Windows + steps: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - powershell: ./eng/scripts/CodeCheck.ps1 -ci $(_InternalRuntimeDownloadArgs) + displayName: Run eng/scripts/CodeCheck.ps1 + artifacts: + - name: Code_Check_Logs + path: artifacts/log/ + publishOnError: true + includeForks: true # Build Windows (x64/x86) - template: jobs/default-build.yml @@ -594,7 +596,7 @@ stages: parameters: condition: ne(variables['SkipTests'], 'true') jobName: MacOS_Test - jobDisplayName: "Test: macOS 10.13" + jobDisplayName: "Test: macOS 10.14" agentOs: macOS isTestingJob: true buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000000..775f221c98 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant +to clarify expected behavior in our community. + +For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 238c74186d..a125150797 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,4 +57,4 @@ Your pull request will now go through extensive checks by the subject matter exp ## Code of conduct -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +See [CODE-OF-CONDUCT.md](./CODE-OF-CONDUCT.md) diff --git a/Directory.Build.props b/Directory.Build.props index 684bab1184..6e658ac596 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -189,7 +189,6 @@ .tar.gz .zip - .sha512 diff --git a/README.md b/README.md index 4c2e975c42..327c3e7208 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Follow the [Getting Started](https://docs.microsoft.com/aspnet/core/getting-star Also check out the [.NET Homepage](https://www.microsoft.com/net) for released versions of .NET, getting started guides, and learning resources. -See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/anurse/issue-policies/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues. +See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/master/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues. ## How to Engage, Contribute, and Give Feedback @@ -37,4 +37,4 @@ These are some other repos for related projects: ## Code of conduct -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +See [CODE-OF-CONDUCT](./CODE-OF-CONDUCT.md) diff --git a/eng/AfterSigning.targets b/eng/AfterSigning.targets new file mode 100644 index 0000000000..4bbb0dcf03 --- /dev/null +++ b/eng/AfterSigning.targets @@ -0,0 +1,33 @@ + + + + + + + $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) + + $(ArtifactsDir)\installers\ + + + + + + + + + + + + + %(FullPath).sha512 + + + + + + + + diff --git a/eng/Build.props b/eng/Build.props index 6f1b3f1908..ef89408b47 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -152,6 +152,7 @@ $(RepoRoot)src\SiteExtensions\LoggingAggregate\test\**\*.csproj; $(RepoRoot)src\Shared\**\*.*proj; $(RepoRoot)src\Tools\**\*.*proj; + $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj; $(RepoRoot)src\Middleware\**\*.csproj; $(RepoRoot)src\Razor\**\*.*proj; $(RepoRoot)src\Mvc\**\*.*proj; @@ -191,6 +192,7 @@ $(RepoRoot)src\Security\**\src\*.csproj; $(RepoRoot)src\SiteExtensions\**\src\*.csproj; $(RepoRoot)src\Tools\**\src\*.csproj; + $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj; $(RepoRoot)src\Middleware\**\src\*.csproj; $(RepoRoot)src\Razor\**\src\*.csproj; $(RepoRoot)src\Mvc\**\src\*.csproj; diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 9fd65b327f..acbfb7e1c9 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -29,7 +29,6 @@ and are generated based on the last package release. - @@ -119,13 +118,11 @@ and are generated based on the last package release. - - - + diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index dda9b06ce8..a7d42c3782 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -32,6 +32,7 @@ + diff --git a/eng/Publishing.props b/eng/Publishing.props index ab7456c178..2c13cb29bf 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -1,7 +1,7 @@ - + - $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) + $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) $(PublishDependsOnTargets);_PublishInstallersAndChecksums @@ -50,12 +50,10 @@ - true diff --git a/eng/SharedFramework.External.props b/eng/SharedFramework.External.props index 9812152edd..59987b0a2c 100644 --- a/eng/SharedFramework.External.props +++ b/eng/SharedFramework.External.props @@ -18,9 +18,8 @@ $(SystemWindowsExtensionsPackageVersion.Split('.')[0]).$(SystemWindowsExtensionsPackageVersion.Split('.')[1]).0 - - + @@ -54,8 +53,6 @@ - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9fde48f524..39a62cd7ee 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -29,185 +29,169 @@ https://github.com/dotnet/aspnetcore-tooling 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 0f28f7168a1a6b1f34ccc4546eb6d5d667fee011 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 - - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 - - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 - - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 - - - https://github.com/dotnet/extensions - 03c40031d618f923aa88da125cb078aabde9ebb1 + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 7090324ea7..b1190c5562 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -65,14 +65,47 @@ 5.0.0-beta.20180.5 3.6.0-3.20201.6 - + 5.0.0-preview.4-runtime.20201.1 5.0.0-preview.4.20201.1 5.0.0-preview.4.20201.1 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 5.0.0-preview.4.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 5.0.0-preview.4.20201.1 5.0.0-preview.4.20201.1 5.0.0-preview.4.20201.1 @@ -95,53 +128,14 @@ 5.0.0-preview.4.20201.1 3.2.0-preview1.20067.1 - - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.2 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 - 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 5.0.0-preview.4.20201.4 5.0.0-preview.4.20201.4 diff --git a/eng/helix/content/RunTests/Program.cs b/eng/helix/content/RunTests/Program.cs index 1535d6663e..f9d3ec2752 100644 --- a/eng/helix/content/RunTests/Program.cs +++ b/eng/helix/content/RunTests/Program.cs @@ -14,222 +14,41 @@ namespace RunTests { static async Task Main(string[] args) { - var command = new RootCommand() + try { - new Option( - aliases: new string[] { "--target", "-t" }, - description: "The test dll to run") - { Argument = new Argument(), Required = true }, + var runner = new TestRunner(RunTestsOptions.Parse(args)); - new Option( - aliases: new string[] { "--sdk" }, - description: "The version of the sdk being used") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--runtime" }, - description: "The version of the runtime being used") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--queue" }, - description: "The name of the Helix queue being run on") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--arch" }, - description: "The architecture being run on") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--quarantined" }, - description: "Whether quarantined tests should run or not") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--ef" }, - description: "The version of the EF tool to use") - { Argument = new Argument(), Required = true }, - }; - - var parseResult = command.Parse(args); - var target = parseResult.ValueForOption("--target"); - var sdkVersion = parseResult.ValueForOption("--sdk"); - var runtimeVersion = parseResult.ValueForOption("--runtime"); - var helixQueue = parseResult.ValueForOption("--queue"); - var architecture = parseResult.ValueForOption("--arch"); - var quarantined = parseResult.ValueForOption("--quarantined"); - var efVersion = parseResult.ValueForOption("--ef"); - - var HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"); - - var path = Environment.GetEnvironmentVariable("PATH"); - var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - - // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project - // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state - if (!File.Exists("NuGet.config")) - { - File.Copy("default.NuGet.config", "NuGet.config"); - } - - var environmentVariables = new Dictionary(); - environmentVariables.Add("PATH", path); - environmentVariables.Add("DOTNET_ROOT", dotnetRoot); - environmentVariables.Add("helix", helixQueue); - - Console.WriteLine($"Current Directory: {HELIX_WORKITEM_ROOT}"); - var helixDir = HELIX_WORKITEM_ROOT; - Console.WriteLine($"Setting HELIX_DIR: {helixDir}"); - environmentVariables.Add("HELIX_DIR", helixDir); - environmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir); - var nugetRestore = Path.Combine(helixDir, "nugetRestore"); - Console.WriteLine($"Creating nuget restore directory: {nugetRestore}"); - environmentVariables.Add("NUGET_RESTORE", nugetRestore); - var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{efVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe"); - Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}"); - environmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath); - - Console.WriteLine("Checking for Microsoft.AspNetCore.App/"); - if (Directory.Exists("Microsoft.AspNetCore.App")) - { - Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}"); - foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories)) + var keepGoing = runner.SetupEnvironment(); + if (keepGoing) { - File.Copy(file, $"{dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}", overwrite: true); + keepGoing = await runner.InstallAspNetAppIfNeededAsync(); } - Console.WriteLine($"Adding current directory to nuget sources: {HELIX_WORKITEM_ROOT}"); + runner.DisplayContents(); - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"nuget add source {HELIX_WORKITEM_ROOT} --configfile NuGet.config", - environmentVariables: environmentVariables); + if (keepGoing) + { + if (!await runner.CheckTestDiscoveryAsync()) + { + Console.WriteLine("RunTest stopping due to test discovery failure."); + Environment.Exit(1); + return; + } - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config", - environmentVariables: environmentVariables); + var exitCode = await runner.RunTestsAsync(); + runner.UploadResults(); + Console.WriteLine($"Completed Helix job with exit code '{exitCode}'"); + Environment.Exit(exitCode); + } - // Write nuget sources to console, useful for debugging purposes - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - "nuget list source", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.WriteLine); - - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"tool install dotnet-ef --global --version {efVersion}", - environmentVariables: environmentVariables); - - // ';' is the path separator on Windows, and ':' on Unix - path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; - path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools"; - environmentVariables["PATH"] = path; - } - - Directory.CreateDirectory(nugetRestore); - - // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project - if (!File.Exists("xunit.runner.json")) - { - File.Copy("default.runner.json", "xunit.runner.json"); - } - - Console.WriteLine(); - Console.WriteLine("Displaying directory contents:"); - foreach (var file in Directory.EnumerateFiles("./")) - { - Console.WriteLine(Path.GetFileName(file)); - } - foreach (var file in Directory.EnumerateDirectories("./")) - { - Console.WriteLine(Path.GetFileName(file)); - } - Console.WriteLine(); - - // Run test discovery so we know if there are tests to run - var discoveryResult = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"vstest {target} -lt", - environmentVariables: environmentVariables); - - if (discoveryResult.StandardOutput.Contains("Exception thrown")) - { - Console.WriteLine("Exception thrown during test discovery."); - Console.WriteLine(discoveryResult.StandardOutput); + Console.WriteLine("Tests were not run due to previous failures. Exit code=1"); Environment.Exit(1); - return; } - - var exitCode = 0; - var commonTestArgs = $"vstest {target} --logger:xunit --logger:\"console;verbosity=normal\" --blame"; - if (quarantined) + catch (Exception e) { - Console.WriteLine("Running quarantined tests."); - - // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md - var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.WriteLine, - throwOnError: false); - - if (result.ExitCode != 0) - { - Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}."); - } + Console.WriteLine($"RunTests uncaught exception: {e.ToString()}"); + Environment.Exit(1); } - else - { - Console.WriteLine("Running non-quarantined tests."); - - // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md - var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.Error.WriteLine, - throwOnError: false); - - if (result.ExitCode != 0) - { - Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}."); - exitCode = result.ExitCode; - } - } - - // 'testResults.xml' is the file Helix looks for when processing test results - Console.WriteLine(); - if (File.Exists("TestResults/TestResults.xml")) - { - Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml"); - File.Copy("TestResults/TestResults.xml", "testResults.xml"); - } - else - { - Console.WriteLine("No test results found."); - } - - var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); - Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/"); - if (Directory.Exists("artifacts/log")) - { - foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories)) - { - // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects - var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}"; - Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}"); - // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs - File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)); - File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName)); - } - } - else - { - Console.WriteLine("No logs found in artifacts/log"); - } - - Console.WriteLine("Completed Helix job."); - Environment.Exit(exitCode); } } } diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs new file mode 100644 index 0000000000..fcfdc84e42 --- /dev/null +++ b/eng/helix/content/RunTests/RunTestsOptions.cs @@ -0,0 +1,81 @@ +// 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.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class RunTestsOptions + { + public static RunTestsOptions Parse(string[] args) + { + var command = new RootCommand() + { + new Option( + aliases: new string[] { "--target", "-t" }, + description: "The test dll to run") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--sdk" }, + description: "The version of the sdk being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--runtime" }, + description: "The version of the runtime being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--queue" }, + description: "The name of the Helix queue being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--arch" }, + description: "The architecture being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--quarantined" }, + description: "Whether quarantined tests should run or not") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--ef" }, + description: "The version of the EF tool to use") + { Argument = new Argument(), Required = true }, + }; + + var parseResult = command.Parse(args); + var options = new RunTestsOptions(); + options.Target = parseResult.ValueForOption("--target"); + options.SdkVersion = parseResult.ValueForOption("--sdk"); + options.RuntimeVersion = parseResult.ValueForOption("--runtime"); + options.HelixQueue = parseResult.ValueForOption("--queue"); + options.Architecture = parseResult.ValueForOption("--arch"); + options.Quarantined = parseResult.ValueForOption("--quarantined"); + options.EfVersion = parseResult.ValueForOption("--ef"); + options.HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"); + options.Path = Environment.GetEnvironmentVariable("PATH"); + options.DotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + return options; + } + + public string Target { get; set;} + public string SdkVersion { get; set;} + public string RuntimeVersion { get; set;} + public string HelixQueue { get; set;} + public string Architecture { get; set;} + public bool Quarantined { get; set;} + public string EfVersion { get; set;} + public string HELIX_WORKITEM_ROOT { get; set;} + public string DotnetRoot { get; set; } + public string Path { get; set; } + } +} diff --git a/eng/helix/content/RunTests/TestRunner.cs b/eng/helix/content/RunTests/TestRunner.cs new file mode 100644 index 0000000000..850d0b629d --- /dev/null +++ b/eng/helix/content/RunTests/TestRunner.cs @@ -0,0 +1,251 @@ +// 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.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class TestRunner + { + public TestRunner(RunTestsOptions options) + { + Options = options; + EnvironmentVariables = new Dictionary(); + } + + public RunTestsOptions Options { get; set; } + public Dictionary EnvironmentVariables { get; set; } + + public bool SetupEnvironment() + { + try + { + // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project + // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state + if (!File.Exists("NuGet.config")) + { + File.Copy("default.NuGet.config", "NuGet.config"); + } + + EnvironmentVariables.Add("PATH", Options.Path); + EnvironmentVariables.Add("DOTNET_ROOT", Options.DotnetRoot); + EnvironmentVariables.Add("helix", Options.HelixQueue); + + Console.WriteLine($"Current Directory: {Options.HELIX_WORKITEM_ROOT}"); + var helixDir = Options.HELIX_WORKITEM_ROOT; + Console.WriteLine($"Setting HELIX_DIR: {helixDir}"); + EnvironmentVariables.Add("HELIX_DIR", helixDir); + EnvironmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir); + var nugetRestore = Path.Combine(helixDir, "nugetRestore"); + EnvironmentVariables.Add("NUGET_RESTORE", nugetRestore); + var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{Options.EfVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe"); + Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}"); + EnvironmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath); + + Console.WriteLine($"Creating nuget restore directory: {nugetRestore}"); + Directory.CreateDirectory(nugetRestore); + + // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project + if (!File.Exists("xunit.runner.json")) + { + File.Copy("default.runner.json", "xunit.runner.json"); + } + + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in SetupEnvironment: {e.ToString()}"); + return false; + } + } + + public void DisplayContents() + { + try + { + Console.WriteLine(); + Console.WriteLine("Displaying directory contents:"); + foreach (var file in Directory.EnumerateFiles("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + foreach (var file in Directory.EnumerateDirectories("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + Console.WriteLine(); + } + catch (Exception e) + { + Console.WriteLine($"Exception in DisplayInitialState: {e.ToString()}"); + } + } + + public async Task InstallAspNetAppIfNeededAsync() + { + try + { + Console.WriteLine("Checking for Microsoft.AspNetCore.App/"); + if (Directory.Exists("Microsoft.AspNetCore.App")) + { + var appRuntimePath = $"{Options.DotnetRoot}/shared/Microsoft.AspNetCore.App/{Options.RuntimeVersion}"; + Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {appRuntimePath}"); + foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories)) + { + File.Copy(file, Path.Combine(appRuntimePath, file), overwrite: true); + } + + Console.WriteLine($"Adding current directory to nuget sources: {Options.HELIX_WORKITEM_ROOT}"); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"nuget add source {Options.HELIX_WORKITEM_ROOT} --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + // Write nuget sources to console, useful for debugging purposes + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget list source", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"tool install dotnet-ef --global --version {Options.EfVersion}", + environmentVariables: EnvironmentVariables); + + // ';' is the path separator on Windows, and ':' on Unix + Options.Path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + Options.Path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools"; + EnvironmentVariables["PATH"] = Options.Path; + } + else + { + Console.WriteLine($"No app runtime found, skipping..."); + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in InstallAspNetAppIfNeeded: {e.ToString()}"); + return false; + } + } + + public async Task CheckTestDiscoveryAsync() + { + try + { + // Run test discovery so we know if there are tests to run + var discoveryResult = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"vstest {Options.Target} -lt", + environmentVariables: EnvironmentVariables); + + if (discoveryResult.StandardOutput.Contains("Exception thrown")) + { + Console.WriteLine("Exception thrown during test discovery."); + Console.WriteLine(discoveryResult.StandardOutput); + return false; + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in CheckTestDiscovery: {e.ToString()}"); + return false; + } + } + + public async Task RunTestsAsync() + { + var exitCode = 0; + try + { + var commonTestArgs = $"vstest {Options.Target} --logger:xunit --logger:\"console;verbosity=normal\" --blame"; + if (Options.Quarantined) + { + Console.WriteLine("Running quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}."); + } + } + else + { + Console.WriteLine("Running non-quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.Error.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}."); + exitCode = result.ExitCode; + } + } + } + catch (Exception e) + { + Console.WriteLine($"Exception in RunTests: {e.ToString()}"); + exitCode = 1; + } + return exitCode; + } + + public void UploadResults() + { + // 'testResults.xml' is the file Helix looks for when processing test results + Console.WriteLine("Trying to upload results..."); + if (File.Exists("TestResults/TestResults.xml")) + { + Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml"); + File.Copy("TestResults/TestResults.xml", "testResults.xml"); + } + else + { + Console.WriteLine("No test results found."); + } + + var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/"); + if (Directory.Exists("artifacts/log")) + { + foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories)) + { + // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects + var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}"; + Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}"); + // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)); + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName)); + } + } + else + { + Console.WriteLine("No logs found in artifacts/log"); + } + } + } +} diff --git a/eng/helix/content/runtests.cmd b/eng/helix/content/runtests.cmd index d71ff1b6ce..1078def35b 100644 --- a/eng/helix/content/runtests.cmd +++ b/eng/helix/content/runtests.cmd @@ -21,10 +21,12 @@ echo "Installing Runtime" powershell.exe -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -Architecture %$arch% -Runtime dotnet -Version %$runtimeVersion% -InstallDir %DOTNET_ROOT%" set exit_code=0 +echo "Restore for RunTests..." dotnet restore RunTests\RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." dotnet run --project RunTests\RunTests.csproj -- --target %1 --sdk %2 --runtime %3 --queue %4 --arch %5 --quarantined %6 --ef %7 if errorlevel 1 ( set exit_code=1 ) - +echo "Finished running tests: exit_code=%exit_code%" exit /b %exit_code% diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh index a8665ab51c..2ce725910c 100755 --- a/eng/helix/content/runtests.sh +++ b/eng/helix/content/runtests.sh @@ -86,7 +86,10 @@ fi sync exit_code=0 +echo "Restore for RunTests..." $DOTNET_ROOT/dotnet restore RunTests/RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." $DOTNET_ROOT/dotnet run --project RunTests/RunTests.csproj -- --target $1 --sdk $2 --runtime $3 --queue $4 --arch $5 --quarantined $6 --ef $7 - -exit $? +exit_code = $? +echo "Finished tests...exit_code=$exit_code" +exit $exit_code diff --git a/eng/scripts/KillProcesses.ps1 b/eng/scripts/KillProcesses.ps1 index 153234abd9..43d7c5ed92 100644 --- a/eng/scripts/KillProcesses.ps1 +++ b/eng/scripts/KillProcesses.ps1 @@ -38,6 +38,15 @@ function _killSeleniumTrackedProcesses() { } } +function _listProcesses($processName) { + $processes = Get-WmiObject win32_process -Filter "name like '%$processName'" -ErrorAction SilentlyContinue; + if ($processes) { + Write-Host "These processes will be killed..." + $processes | select commandline | Out-String -Width 800 + } +} + +_listProcesses dotnet _kill dotnet.exe _kill testhost.exe _kill iisexpress.exe diff --git a/eng/scripts/KillProcesses.sh b/eng/scripts/KillProcesses.sh index f52511739b..e491dc1693 100755 --- a/eng/scripts/KillProcesses.sh +++ b/eng/scripts/KillProcesses.sh @@ -1,4 +1,12 @@ #!/usr/bin/env bash +# list processes that would be killed so they appear in the log +p=$(pgrep dotnet) +if [ $? -eq 0 ] +then + echo "These processes will be killed..." + ps -p $p +fi + pkill dotnet || true exit 0 diff --git a/eng/targets/ResolveReferences.targets b/eng/targets/ResolveReferences.targets index 37a17b7140..8cc5c717ec 100644 --- a/eng/targets/ResolveReferences.targets +++ b/eng/targets/ResolveReferences.targets @@ -216,16 +216,16 @@ Condition=" '$(CompileUsingReferenceAssemblies)' != false AND '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">true - + IncludeAssets="Compile" + PrivateAssets="All" + GeneratePathProperty="true" /> diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 77e0b06faa..5ecddb3ed6 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2810,8 +2810,7 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Equal(10, component.OnAfterRenderCallCount); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/7487")] + [Fact] public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync() { // This represents the scenario where the same event handler is being triggered diff --git a/src/Components/test/testassets/TestServer/Components.TestServer.csproj b/src/Components/test/testassets/TestServer/Components.TestServer.csproj index 4eebc1f24b..d51a812210 100644 --- a/src/Components/test/testassets/TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/TestServer/Components.TestServer.csproj @@ -14,8 +14,8 @@ + - diff --git a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj index f695e4d3ee..4f60448f35 100644 --- a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj @@ -53,6 +53,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion).0 $(ReferencePackSharedFxVersion)-$(VersionSuffix) + + + $(PkgMicrosoft_Extensions_Internal_Transport)\ref\$(TargetFramework)\ + @@ -109,14 +113,14 @@ This package is an internal implementation of the .NET Core SDK and is not meant BeforeTargets="_GetPackageFiles" DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences"> - <_AvailableExtensionsRefAssemblies Include="$(MicrosoftInternalExtensionsRefsPath)\*.dll" /> + <_AvailableExtensionsRefAssemblies Include="$(RuntimeExtensionsReferenceDirectory)*.dll" /> - + @@ -131,7 +135,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant - + <_ReferencedExtensionsRefAssembliesToExclude Include="@(_ReferencedExtensionsRefAssemblies)" Exclude="@(_DuplicatedExtensionsRefAssemblies)" /> @@ -147,10 +151,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant @(ReferencePathWithRefAssemblies->WithMetadataValue('ReferenceGrouping', 'Microsoft.NETCore.App'));" /> + Include="@(_SelectedExtensionsRefAssemblies->'$(RuntimeExtensionsReferenceDirectory)%(FileName)%(Extension)')" /> - + @@ -165,7 +169,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant Outputs="$(TargetDir)$(PackageConflictManifestFileName)"> - <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(MicrosoftInternalExtensionsRefsPath)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Internal.Extensions.Refs' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " /> + <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(RuntimeExtensionsReferenceDirectory)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Extensions.Internal.Transport' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " /> <_AspNetCoreAppPackageOverrides Include="@(_SelectedExtensionsRefAssemblies->'%(FileName)|$(MicrosoftInternalExtensionsRefsPackageOverrideVersion)')" /> diff --git a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj index 84b237a6ca..7397e945bf 100644 --- a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj +++ b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj @@ -156,12 +156,6 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(InstallersOutputPath)$(RedistArchiveOutputFileName) - - - $(RedistArchiveOutputPath)$(ChecksumExtension) - - - diff --git a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj index 9abf901de4..8d9b26e7d0 100644 --- a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj +++ b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj @@ -36,6 +36,10 @@ <_Parameter1>TargetingPackLayoutRoot <_Parameter2>$(TargetingPackLayoutRoot) + + <_Parameter1>IsTargetingPackBuilding + <_Parameter2>$(IsTargetingPackBuilding) + <_Parameter1>VerifyAncmBinary <_Parameter2>$(VerifyAncmBinary) diff --git a/src/Framework/test/TargetingPackTests.cs b/src/Framework/test/TargetingPackTests.cs index 82b43cb831..54da276513 100644 --- a/src/Framework/test/TargetingPackTests.cs +++ b/src/Framework/test/TargetingPackTests.cs @@ -20,17 +20,24 @@ namespace Microsoft.AspNetCore private readonly string _expectedRid; private readonly string _targetingPackRoot; private readonly ITestOutputHelper _output; + private readonly bool _isTargetingPackBuilding; public TargetingPackTests(ITestOutputHelper output) { _output = output; _expectedRid = TestData.GetSharedFxRuntimeIdentifier(); _targetingPackRoot = Path.Combine(TestData.GetTestDataValue("TargetingPackLayoutRoot"), "packs", "Microsoft.AspNetCore.App.Ref", TestData.GetTestDataValue("TargetingPackVersion")); + _isTargetingPackBuilding = bool.Parse(TestData.GetTestDataValue("IsTargetingPackBuilding")); } [Fact] public void AssembliesAreReferenceAssemblies() { + if (!_isTargetingPackBuilding) + { + return; + } + IEnumerable dlls = Directory.GetFiles(_targetingPackRoot, "*.dll", SearchOption.AllDirectories); Assert.NotEmpty(dlls); @@ -58,6 +65,11 @@ namespace Microsoft.AspNetCore [Fact] public void PlatformManifestListsAllFiles() { + if (!_isTargetingPackBuilding) + { + return; + } + var platformManifestPath = Path.Combine(_targetingPackRoot, "data", "PlatformManifest.txt"); var expectedAssemblies = TestData.GetSharedFxDependencies() .Split(';', StringSplitOptions.RemoveEmptyEntries) diff --git a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index d6b79361e7..4f83bbda83 100644 --- a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -9,6 +9,7 @@ + @@ -16,6 +17,7 @@ + diff --git a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index a737e01d45..44e1d8d95f 100644 --- a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -28,6 +28,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index ba625f4332..939dfcf376 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -23,7 +23,6 @@ - diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs index 96ba91bcaa..58abe91b33 100644 --- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } + [QuarantinedTest] [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows)] [OSSkipCondition(OperatingSystems.MacOSX)] @@ -133,7 +134,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests private static void WaitForExitOrKill(Process process) { - process.WaitForExit(1000); + process.WaitForExit(5 * 1000); if (!process.HasExited) { process.Kill(); diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs index 099fdf73e5..dda0233d77 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs @@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer X509KeyStorageFlags.DefaultKeySet); [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public void Configure_AddsDevelopmentKeyFromConfiguration() { @@ -64,7 +63,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void Configure_LoadsPfxCertificateCredentialFromConfiguration() { // Arrange @@ -94,7 +93,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void Configure_LoadsCertificateStoreCertificateCredentialFromConfiguration() { try diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs index c5d543dfe9..893be873ab 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public static void LoadFromStoreCert_SkipsCertificatesNotYetValid() { try @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public static void LoadFromStoreCert_PrefersCertificatesCloserToExpirationDate() { try @@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public static void LoadFromStoreCert_SkipsExpiredCertificates() { try diff --git a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj index 1ddecfe522..846f73feee 100644 --- a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj +++ b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj index 7555dda1fe..5f6681e261 100644 --- a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj +++ b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj @@ -13,7 +13,8 @@ - + + diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md index 77fc8404c4..511b1088fc 100644 --- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md @@ -2,17 +2,3 @@ Microsoft IIS Common -------------------------------- The repository contains common resources shared by IIS Out-Of-Band (OOB) products. - -### Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md index 22e594aa6c..5a4135df83 100644 --- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md @@ -2,17 +2,3 @@ Microsoft IIS Setup -------------------------------- The repository contains setup resources shared by IIS Out-Of-Band (OOB) products. - -### Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/src/Installers/Windows/Wix.targets b/src/Installers/Windows/Wix.targets index 2e4b8dca03..de6636d51d 100644 --- a/src/Installers/Windows/Wix.targets +++ b/src/Installers/Windows/Wix.targets @@ -64,7 +64,7 @@ + BeforeTargets="Build"> <_cabs Include="$(TargetDir)**/*.cab" /> @@ -72,10 +72,4 @@ - - - $(InstallersOutputPath)$(PackageFileName)$(ChecksumExtension) - - - diff --git a/src/Logging.AzureAppServices/Directory.Build.props b/src/Logging.AzureAppServices/Directory.Build.props new file mode 100644 index 0000000000..68f87d4f24 --- /dev/null +++ b/src/Logging.AzureAppServices/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + true + + diff --git a/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs new file mode 100644 index 0000000000..9b680e9138 --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging.AzureAppServices; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Options; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Extension methods for adding Azure diagnostics logger. + /// + public static class AzureAppServicesLoggerFactoryExtensions + { + /// + /// Adds an Azure Web Apps diagnostics logger. + /// + /// The extension method argument + public static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder) + { + var context = WebAppContext.Default; + + // Only add the provider if we're in Azure WebApp. That cannot change once the apps started + return AddAzureWebAppDiagnostics(builder, context); + } + + internal static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder, IWebAppContext context) + { + if (!context.IsRunningInAzureWebApp) + { + return builder; + } + + builder.AddConfiguration(); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context); + var services = builder.Services; + + var addedFileLogger = TryAddEnumerable(services, Singleton()); + var addedBlobLogger = TryAddEnumerable(services, Singleton()); + + if (addedFileLogger || addedBlobLogger) + { + services.AddSingleton(context); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + } + + if (addedFileLogger) + { + services.AddSingleton>(CreateFileFilterConfigureOptions(config)); + services.AddSingleton>(new FileLoggerConfigureOptions(config, context)); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + LoggerProviderOptions.RegisterProviderOptions(builder.Services); + } + + if (addedBlobLogger) + { + services.AddSingleton>(CreateBlobFilterConfigureOptions(config)); + services.AddSingleton>(new BlobLoggerConfigureOptions(config, context)); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + LoggerProviderOptions.RegisterProviderOptions(builder.Services); + } + + return builder; + } + + private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor) + { + var beforeCount = collection.Count; + collection.TryAddEnumerable(descriptor); + return beforeCount != collection.Count; + } + + private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config) + { + return new ConfigurationBasedLevelSwitcher( + configuration: config, + provider: typeof(BlobLoggerProvider), + levelKey: "AzureBlobTraceLevel"); + } + + private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config) + { + return new ConfigurationBasedLevelSwitcher( + configuration: config, + provider: typeof(FileLoggerProvider), + levelKey: "AzureDriveTraceLevel"); + } + } +} diff --git a/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs new file mode 100644 index 0000000000..1e1285b358 --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for Azure diagnostics blob logging. + /// + public class AzureBlobLoggerOptions: BatchingLoggerOptions + { + private string _blobName = "applicationLog.txt"; + + /// + /// Gets or sets the last section of log blob name. + /// Defaults to "applicationLog.txt". + /// + public string BlobName + { + get { return _blobName; } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string."); + } + _blobName = value; + } + } + + internal string ContainerUrl { get; set; } + + internal string ApplicationName { get; set; } + + internal string ApplicationInstanceId { get; set; } + } +} diff --git a/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs new file mode 100644 index 0000000000..af8b5a112e --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for Azure diagnostics file logging. + /// + public class AzureFileLoggerOptions: BatchingLoggerOptions + { + private int? _fileSizeLimit = 10 * 1024 * 1024; + private int? _retainedFileCountLimit = 2; + private string _fileName = "diagnostics-"; + + /// + /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. + /// Once the log is full, no more messages will be appended. + /// Defaults to 10MB. + /// + public int? FileSizeLimit + { + get { return _fileSizeLimit; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive."); + } + _fileSizeLimit = value; + } + } + + /// + /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit. + /// Defaults to 2. + /// + public int? RetainedFileCountLimit + { + get { return _retainedFileCountLimit; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive."); + } + _retainedFileCountLimit = value; + } + } + + /// + /// Gets or sets a string representing the prefix of the file name used to store the logging information. + /// The current date, in the format YYYYMMDD will be added after the given value. + /// Defaults to diagnostics-. + /// + public string FileName + { + get { return _fileName; } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + _fileName = value; + } + } + + internal string LogDirectory { get; set; } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs new file mode 100644 index 0000000000..8dc8727b3a --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BatchLoggerConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly string _isEnabledKey; + + public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey) + { + _configuration = configuration; + _isEnabledKey = isEnabledKey; + } + + public void Configure(BatchingLoggerOptions options) + { + options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value); + } + + private static bool TextToBoolean(string text) + { + if (string.IsNullOrEmpty(text) || + !bool.TryParse(text, out var result)) + { + result = false; + } + + return result; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLogger.cs b/src/Logging.AzureAppServices/src/BatchingLogger.cs new file mode 100644 index 0000000000..bd192169f3 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLogger.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BatchingLogger : ILogger + { + private readonly BatchingLoggerProvider _provider; + private readonly string _category; + + public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName) + { + _provider = loggerProvider; + _category = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return _provider.IsEnabled; + } + + public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var builder = new StringBuilder(); + builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); + builder.Append(" ["); + builder.Append(logLevel.ToString()); + builder.Append("] "); + builder.Append(_category); + + var scopeProvider = _provider.ScopeProvider; + if (scopeProvider != null) + { + scopeProvider.ForEachScope((scope, stringBuilder) => + { + stringBuilder.Append(" => ").Append(scope); + }, builder); + + builder.AppendLine(":"); + } + else + { + builder.Append(": "); + } + + builder.AppendLine(formatter(state, exception)); + + if (exception != null) + { + builder.AppendLine(exception.ToString()); + } + + _provider.AddMessage(timestamp, builder.ToString()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); + } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs new file mode 100644 index 0000000000..9fbd964800 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for a logger which batches up log messages. + /// + public class BatchingLoggerOptions + { + private int? _batchSize; + private int? _backgroundQueueSize = 1000; + private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the period after which logs will be flushed to the store. + /// + public TimeSpan FlushPeriod + { + get { return _flushPeriod; } + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive."); + } + _flushPeriod = value; + } + } + + /// + /// Gets or sets the maximum size of the background log message queue or null for no limit. + /// After maximum queue size is reached log event sink would start blocking. + /// Defaults to 1000. + /// + public int? BackgroundQueueSize + { + get { return _backgroundQueueSize; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative."); + } + _backgroundQueueSize = value; + } + } + + /// + /// Gets or sets a maximum number of events to include in a single batch or null for no limit. + /// + /// Defaults to null. + public int? BatchSize + { + get { return _batchSize; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive."); + } + _batchSize = value; + } + } + + /// + /// Gets or sets value indicating if logger accepts and queues writes. + /// + public bool IsEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether scopes should be included in the message. + /// Defaults to false. + /// + public bool IncludeScopes { get; set; } = false; + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs new file mode 100644 index 0000000000..227a616f3b --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// A provider of instances. + /// + public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope + { + private readonly List _currentBatch = new List(); + private readonly TimeSpan _interval; + private readonly int? _queueSize; + private readonly int? _batchSize; + private readonly IDisposable _optionsChangeToken; + + private int _messagesDropped; + + private BlockingCollection _messageQueue; + private Task _outputTask; + private CancellationTokenSource _cancellationTokenSource; + + private bool _includeScopes; + private IExternalScopeProvider _scopeProvider; + + internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null; + + internal BatchingLoggerProvider(IOptionsMonitor options) + { + // NOTE: Only IsEnabled is monitored + + var loggerOptions = options.CurrentValue; + if (loggerOptions.BatchSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number."); + } + if (loggerOptions.FlushPeriod <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); + } + + _interval = loggerOptions.FlushPeriod; + _batchSize = loggerOptions.BatchSize; + _queueSize = loggerOptions.BackgroundQueueSize; + + _optionsChangeToken = options.OnChange(UpdateOptions); + UpdateOptions(options.CurrentValue); + } + + /// + /// Checks if the queue is enabled. + /// + public bool IsEnabled { get; private set; } + + private void UpdateOptions(BatchingLoggerOptions options) + { + var oldIsEnabled = IsEnabled; + IsEnabled = options.IsEnabled; + _includeScopes = options.IncludeScopes; + + if (oldIsEnabled != IsEnabled) + { + if (IsEnabled) + { + Start(); + } + else + { + Stop(); + } + } + + } + + internal abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); + + private async Task ProcessLogQueue() + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + var limit = _batchSize ?? int.MaxValue; + + while (limit > 0 && _messageQueue.TryTake(out var message)) + { + _currentBatch.Add(message); + limit--; + } + + var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0); + if (messagesDropped != 0) + { + _currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}")); + } + + if (_currentBatch.Count > 0) + { + try + { + await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token); + } + catch + { + // ignored + } + + _currentBatch.Clear(); + } + else + { + await IntervalAsync(_interval, _cancellationTokenSource.Token); + } + } + } + + /// + /// Wait for the given . + /// + /// The amount of time to wait. + /// A that can be used to cancel the delay. + /// A which completes when the has passed or the has been canceled. + protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return Task.Delay(interval, cancellationToken); + } + + internal void AddMessage(DateTimeOffset timestamp, string message) + { + if (!_messageQueue.IsAddingCompleted) + { + try + { + if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token)) + { + Interlocked.Increment(ref _messagesDropped); + } + } + catch + { + //cancellation token canceled or CompleteAdding called + } + } + } + + private void Start() + { + _messageQueue = _queueSize == null ? + new BlockingCollection(new ConcurrentQueue()) : + new BlockingCollection(new ConcurrentQueue(), _queueSize.Value); + + _cancellationTokenSource = new CancellationTokenSource(); + _outputTask = Task.Run(ProcessLogQueue); + } + + private void Stop() + { + _cancellationTokenSource.Cancel(); + _messageQueue.CompleteAdding(); + + try + { + _outputTask.Wait(_interval); + } + catch (TaskCanceledException) + { + } + catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) + { + } + } + + /// + public void Dispose() + { + _optionsChangeToken?.Dispose(); + if (IsEnabled) + { + Stop(); + } + } + + /// + /// Creates a with the given . + /// + /// The name of the category to create this logger with. + /// The that was created. + public ILogger CreateLogger(string categoryName) + { + return new BatchingLogger(this, categoryName); + } + + /// + /// Sets the scope on this provider. + /// + /// Provides the scope. + void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs new file mode 100644 index 0000000000..e9805128b7 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + internal class BlobAppendReferenceWrapper : ICloudAppendBlob + { + private readonly Uri _fullUri; + private readonly HttpClient _client; + private readonly Uri _appendUri; + + public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client) + { + var uriBuilder = new UriBuilder(containerUrl); + uriBuilder.Path += "/" + name; + _fullUri = uriBuilder.Uri; + + AppendBlockQuery(uriBuilder); + _appendUri = uriBuilder.Uri; + _client = client; + } + + /// + public async Task AppendAsync(ArraySegment data, CancellationToken cancellationToken) + { + Task AppendDataAsync() + { + var message = new HttpRequestMessage(HttpMethod.Put, _appendUri) + { + Content = new ByteArrayContent(data.Array, data.Offset, data.Count) + }; + AddCommonHeaders(message); + + return _client.SendAsync(message, cancellationToken); + } + + var response = await AppendDataAsync(); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + // If no blob exists try creating it + var message = new HttpRequestMessage(HttpMethod.Put, _fullUri) + { + // Set Content-Length to 0 to create "Append Blob" + Content = new ByteArrayContent(Array.Empty()), + Headers = + { + { "If-None-Match", "*" } + } + }; + + AddCommonHeaders(message); + + response = await _client.SendAsync(message, cancellationToken); + + // If result is 2** or 412 try to append again + if (response.IsSuccessStatusCode || + response.StatusCode == HttpStatusCode.PreconditionFailed) + { + // Retry sending data after blob creation + response = await AppendDataAsync(); + } + } + + response.EnsureSuccessStatusCode(); + } + + private static void AddCommonHeaders(HttpRequestMessage message) + { + message.Headers.Add("x-ms-blob-type", "AppendBlob"); + message.Headers.Add("x-ms-version", "2016-05-31"); + message.Headers.Date = DateTimeOffset.UtcNow; + } + + private static void AppendBlockQuery(UriBuilder uriBuilder) + { + // See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for: + // Note: Do not append a string directly to Query property. + // If the length of Query is greater than 1, retrieve the property value + // as a string, remove the leading question mark, append the new query string, + // and set the property with the combined string. + var queryToAppend = "comp=appendblock"; + if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend; + else + uriBuilder.Query = queryToAppend; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs new file mode 100644 index 0000000000..f9a186872b --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly IWebAppContext _context; + + public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context) + : base(configuration, "AzureBlobEnabled") + { + _configuration = configuration; + _context = context; + } + + public void Configure(AzureBlobLoggerOptions options) + { + base.Configure(options); + options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value; + options.ApplicationName = _context.SiteName; + options.ApplicationInstanceId = _context.SiteInstanceId; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs new file mode 100644 index 0000000000..3d62ea2ac6 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// The implementation that stores messages by appending them to Azure Blob in batches. + /// + [ProviderAlias("AzureAppServicesBlob")] + public class BlobLoggerProvider : BatchingLoggerProvider + { + private readonly string _appName; + private readonly string _fileName; + private readonly Func _blobReferenceFactory; + private readonly HttpClient _httpClient; + + /// + /// Creates a new instance of + /// + /// The options to use when creating a provider. + public BlobLoggerProvider(IOptionsMonitor options) + : this(options, null) + { + _blobReferenceFactory = name => new BlobAppendReferenceWrapper( + options.CurrentValue.ContainerUrl, + name, + _httpClient); + } + + /// + /// Creates a new instance of + /// + /// The container to store logs to. + /// Options to be used in creating a logger. + internal BlobLoggerProvider( + IOptionsMonitor options, + Func blobReferenceFactory) : + base(options) + { + var value = options.CurrentValue; + _appName = value.ApplicationName; + _fileName = value.ApplicationInstanceId + "_" + value.BlobName; + _blobReferenceFactory = blobReferenceFactory; + _httpClient = new HttpClient(); + } + + internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + var eventGroups = messages.GroupBy(GetBlobKey); + foreach (var eventGroup in eventGroups) + { + var key = eventGroup.Key; + var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}"; + + var blob = _blobReferenceFactory(blobName); + + using (var stream = new MemoryStream()) + using (var writer = new StreamWriter(stream)) + { + foreach (var logEvent in eventGroup) + { + writer.Write(logEvent.Message); + } + + await writer.FlushAsync(); + var tryGetBuffer = stream.TryGetBuffer(out var buffer); + Debug.Assert(tryGetBuffer); + await blob.AppendAsync(buffer, cancellationToken); + } + } + } + + private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e) + { + return (e.Timestamp.Year, + e.Timestamp.Month, + e.Timestamp.Day, + e.Timestamp.Hour); + } + } +} diff --git a/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs new file mode 100644 index 0000000000..c62ccb2331 --- /dev/null +++ b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class ConfigurationBasedLevelSwitcher: IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly Type _provider; + private readonly string _levelKey; + + public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey) + { + _configuration = configuration; + _provider = provider; + _levelKey = levelKey; + } + + public void Configure(LoggerFilterOptions options) + { + options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null)); + } + + private LogLevel GetLogLevel() + { + return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value); + } + + private static LogLevel TextToLogLevel(string text) + { + switch (text?.ToUpperInvariant()) + { + case "ERROR": + return LogLevel.Error; + case "WARNING": + return LogLevel.Warning; + case "INFORMATION": + return LogLevel.Information; + case "VERBOSE": + return LogLevel.Trace; + default: + return LogLevel.None; + } + } + } +} diff --git a/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs new file mode 100644 index 0000000000..8cd1f5eb91 --- /dev/null +++ b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions + { + private readonly IWebAppContext _context; + + public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context) + : base(configuration, "AzureDriveEnabled") + { + _context = context; + } + + public void Configure(AzureFileLoggerOptions options) + { + base.Configure(options); + options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application"); + } + } +} diff --git a/src/Logging.AzureAppServices/src/FileLoggerProvider.cs b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs new file mode 100644 index 0000000000..1143d38c07 --- /dev/null +++ b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// A which writes out to a file. + /// + [ProviderAlias("AzureAppServicesFile")] + public class FileLoggerProvider : BatchingLoggerProvider + { + private readonly string _path; + private readonly string _fileName; + private readonly int? _maxFileSize; + private readonly int? _maxRetainedFiles; + + /// + /// Creates a new instance of . + /// + /// The options to use when creating a provider. + public FileLoggerProvider(IOptionsMonitor options) : base(options) + { + var loggerOptions = options.CurrentValue; + _path = loggerOptions.LogDirectory; + _fileName = loggerOptions.FileName; + _maxFileSize = loggerOptions.FileSizeLimit; + _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; + } + + internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + Directory.CreateDirectory(_path); + + foreach (var group in messages.GroupBy(GetGrouping)) + { + var fullName = GetFullName(group.Key); + var fileInfo = new FileInfo(fullName); + if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) + { + return; + } + + using (var streamWriter = File.AppendText(fullName)) + { + foreach (var item in group) + { + await streamWriter.WriteAsync(item.Message); + } + } + } + + RollFiles(); + } + + private string GetFullName((int Year, int Month, int Day) group) + { + return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt"); + } + + private (int Year, int Month, int Day) GetGrouping(LogMessage message) + { + return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day); + } + + private void RollFiles() + { + if (_maxRetainedFiles > 0) + { + var files = new DirectoryInfo(_path) + .GetFiles(_fileName + "*") + .OrderByDescending(f => f.Name) + .Skip(_maxRetainedFiles.Value); + + foreach (var item in files) + { + item.Delete(); + } + } + } + } +} diff --git a/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs new file mode 100644 index 0000000000..2f55bbb0d1 --- /dev/null +++ b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob. + /// + internal interface ICloudAppendBlob + { + /// + /// Initiates an asynchronous operation to open a stream for writing to the blob. + /// + /// A object of type that represents the asynchronous operation. + Task AppendAsync(ArraySegment data, CancellationToken cancellationToken); + } +} diff --git a/src/Logging.AzureAppServices/src/IWebAppContext.cs b/src/Logging.AzureAppServices/src/IWebAppContext.cs new file mode 100644 index 0000000000..f8c826ceb8 --- /dev/null +++ b/src/Logging.AzureAppServices/src/IWebAppContext.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents an Azure WebApp context + /// + internal interface IWebAppContext + { + /// + /// Gets the path to the home folder if running in Azure WebApp + /// + string HomeFolder { get; } + + /// + /// Gets the name of site if running in Azure WebApp + /// + string SiteName { get; } + + /// + /// Gets the id of site if running in Azure WebApp + /// + string SiteInstanceId { get; } + + /// + /// Gets a value indicating whether or new we're in an Azure WebApp + /// + bool IsRunningInAzureWebApp { get; } + } +} diff --git a/src/Logging.AzureAppServices/src/LogMessage.cs b/src/Logging.AzureAppServices/src/LogMessage.cs new file mode 100644 index 0000000000..4a1179ceb3 --- /dev/null +++ b/src/Logging.AzureAppServices/src/LogMessage.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal readonly struct LogMessage + { + public LogMessage(DateTimeOffset timestamp, string message) + { + Timestamp = timestamp; + Message = message; + } + + public DateTimeOffset Timestamp { get; } + public string Message { get; } + } +} diff --git a/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj new file mode 100644 index 0000000000..5bedde8c6d --- /dev/null +++ b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj @@ -0,0 +1,24 @@ + + + + Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features. + netstandard2.0 + $(NoWarn);CS1591 + true + true + + + + + + + + + + + + + + + + diff --git a/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7c7d332545 --- /dev/null +++ b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs new file mode 100644 index 0000000000..452c936f93 --- /dev/null +++ b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class SiteConfigurationProvider + { + public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context) + { + var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics"); + var settingsFile = Path.Combine(settingsFolder, "settings.json"); + + return new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile(settingsFile, optional: true, reloadOnChange: true) + .Build(); + } + } +} diff --git a/src/Logging.AzureAppServices/src/WebAppContext.cs b/src/Logging.AzureAppServices/src/WebAppContext.cs new file mode 100644 index 0000000000..8bdd3f1c76 --- /dev/null +++ b/src/Logging.AzureAppServices/src/WebAppContext.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents the default implementation of . + /// + internal class WebAppContext : IWebAppContext + { + /// + /// Gets the default instance of the WebApp context. + /// + public static WebAppContext Default { get; } = new WebAppContext(); + + private WebAppContext() { } + + /// + public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME"); + + /// + public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); + + /// + public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"); + + /// + public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) && + !string.IsNullOrEmpty(SiteName); + } +} diff --git a/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs new file mode 100644 index 0000000000..2fd5955e86 --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureAppendBlobTests + { + public string _containerUrl = "https://host/container?query=1"; + public string _blobName = "blob/path"; + + [Fact] + public async Task SendsDataAsStream() + { + var testMessageHandler = new TestMessageHandler(async message => + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + AssertDefaultHeaders(message); + + return new HttpResponseMessage(HttpStatusCode.OK); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None); + } + + private static void AssertDefaultHeaders(HttpRequestMessage message) + { + Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type")); + Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version")); + Assert.NotNull(message.Headers.Date); + } + + [Theory] + [InlineData(HttpStatusCode.Created)] + [InlineData(HttpStatusCode.PreconditionFailed)] + public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode) + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + // Create request + if (stage == 1) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString()); + Assert.Equal(0, message.Content.Headers.ContentLength); + Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match")); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(createStatusCode); + } + // First PUT request + if (stage == 2) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.Created); + } + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None); + + Assert.Equal(3, stage); + } + + [Fact] + public async Task ThrowsForUnknownStatus() + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None)); + + Assert.Equal(1, stage); + } + + [Fact] + public async Task ThrowsForUnknownStatusDuringCreation() + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + // Create request + if (stage == 1) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString()); + Assert.Equal(0, message.Content.Headers.ContentLength); + Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match")); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None)); + + Assert.Equal(2, stage); + } + + + private class TestMessageHandler : HttpMessageHandler + { + private readonly Func> _callback; + + public TestMessageHandler(Func> callback) + { + _callback = callback; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return await _callback(request); + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs new file mode 100644 index 0000000000..4d9125335a --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureBlobSinkTests + { + DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + + [Fact] + public async Task WritesMessagesInBatches() + { + var blob = new Mock(); + var buffers = new List(); + blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny())) + .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s))) + .Returns(Task.CompletedTask); + + var sink = new TestBlobSink(name => blob.Object); + var logger = (BatchingLogger)sink.CreateLogger("Cat"); + + await sink.IntervalControl.Pause; + + for (int i = 0; i < 5; i++) + { + logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state); + } + + sink.IntervalControl.Resume(); + await sink.IntervalControl.Pause; + + Assert.Single(buffers); + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine, + Encoding.UTF8.GetString(buffers[0])); + } + + [Fact] + public async Task GroupsByHour() + { + var blob = new Mock(); + var buffers = new List(); + var names = new List(); + + blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny())) + .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s))) + .Returns(Task.CompletedTask); + + var sink = new TestBlobSink(name => + { + names.Add(name); + return blob.Object; + }); + var logger = (BatchingLogger)sink.CreateLogger("Cat"); + + await sink.IntervalControl.Pause; + + var startDate = _timestampOne; + for (int i = 0; i < 3; i++) + { + logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state); + + startDate = startDate.AddHours(1); + } + + sink.IntervalControl.Resume(); + await sink.IntervalControl.Pause; + + Assert.Equal(3, buffers.Count); + + Assert.Equal("appname/2016/05/04/03/42_filename", names[0]); + Assert.Equal("appname/2016/05/04/04/42_filename", names[1]); + Assert.Equal("appname/2016/05/04/05/42_filename", names[2]); + } + + private byte[] ToArray(ArraySegment inputStream) + { + return inputStream.Array + .Skip(inputStream.Offset) + .Take(inputStream.Count) + .ToArray(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs new file mode 100644 index 0000000000..00d7dcd58d --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureDiagnosticsConfigurationProviderTests + { + [Fact] + public void NoConfigFile() + { + var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist"); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder) + .Returns(tempFolder); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object); + + Assert.NotNull(config); + } + + [Fact] + public void ReadsSettingsFileAndEnvironment() + { + var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile"); + + try + { + var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics"); + var settingsFile = Path.Combine(settingsFolder, "settings.json"); + + if (!Directory.Exists(settingsFolder)) + { + Directory.CreateDirectory(settingsFolder); + } + Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE"); + File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }"); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder) + .Returns(tempFolder); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object); + + Assert.Equal("test value", config["key"]); + Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]); + } + finally + { + if (Directory.Exists(tempFolder)) + { + try + { + Directory.Delete(tempFolder, recursive: true); + } + catch + { + // Don't break the test if temp folder deletion fails. + } + } + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs new file mode 100644 index 0000000000..9ab0c0cb45 --- /dev/null +++ b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class BatchingLoggerProviderTests + { + private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + private string _nl = Environment.NewLine; + private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} "); + + [Fact] + public async Task LogsInIntervals() + { + var provider = new TestBatchingLoggingProvider(); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message); + } + + [Fact] + public async Task IncludesScopes() + { + var provider = new TestBatchingLoggingProvider(includeScopes: true); + var factory = new LoggerFactory(new [] { provider }); + var logger = factory.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + using (logger.BeginScope("Scope")) + { + using (logger.BeginScope("Scope2")) + { + logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state); + } + } + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message); + Assert.EndsWith( + " [Information] Cat => Scope => Scope2:" + _nl + + "Info message" + _nl, + provider.Batches[0][0].Message); + } + + [Fact] + public async Task RespectsBatchSize() + { + var provider = new TestBatchingLoggingProvider(maxBatchSize: 1); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal(2, provider.Batches.Count); + Assert.Single(provider.Batches[0]); + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + + Assert.Single(provider.Batches[1]); + Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message); + } + + [Fact] + public async Task DropsMessagesWhenReachingMaxQueue() + { + var provider = new TestBatchingLoggingProvider(maxQueueSize: 1); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal(2, provider.Batches[0].Length); + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message); + } + + private class TestBatchingLoggingProvider: BatchingLoggerProvider + { + public List Batches { get; } = new List(); + public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false) + : base(new OptionsWrapperMonitor(new BatchingLoggerOptions + { + FlushPeriod = interval ?? TimeSpan.FromSeconds(1), + BatchSize = maxBatchSize, + BackgroundQueueSize = maxQueueSize, + IsEnabled = true, + IncludeScopes = includeScopes + })) + { + } + + internal override Task WriteMessagesAsync(IEnumerable messages, CancellationToken token) + { + Batches.Add(messages.ToArray()); + return Task.CompletedTask; + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs new file mode 100644 index 0000000000..46b72c7a0d --- /dev/null +++ b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class ConfigureOptionsTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public void InitializesIsEnabled(bool? enabled) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("IsEnabledKey", Convert.ToString(enabled)) + }).Build(); + + var options = new BatchingLoggerOptions(); + new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options); + + Assert.Equal(enabled ?? false, options.IsEnabled); + } + + [Fact] + public void InitializesLogDirectory() + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url") + }).Build(); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder).Returns("Home"); + + var options = new AzureFileLoggerOptions(); + new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options); + + Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory); + } + + [Fact] + public void InitializesBlobUriSiteInstanceAndName() + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new [] + { + new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url") + }).Build(); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder).Returns("Home"); + contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId"); + contextMock.SetupGet(c => c.SiteName).Returns("Name"); + + var options = new AzureBlobLoggerOptions(); + new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options); + + Assert.Equal("http://container/url", options.ContainerUrl); + Assert.Equal("InstanceId", options.ApplicationInstanceId); + Assert.Equal("Name", options.ApplicationName); + } + } +} diff --git a/src/Logging.AzureAppServices/test/FileLoggerTests.cs b/src/Logging.AzureAppServices/test/FileLoggerTests.cs new file mode 100644 index 0000000000..a3fcd2587d --- /dev/null +++ b/src/Logging.AzureAppServices/test/FileLoggerTests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class FileLoggerTests: IDisposable + { + DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + + public FileLoggerTests() + { + TempPath = Path.GetTempFileName() + "_"; + } + + public string TempPath { get; } + + public void Dispose() + { + try + { + if (Directory.Exists(TempPath)) + { + Directory.Delete(TempPath, true); + } + } + catch + { + // ignored + } + } + + [Fact] + public async Task WritesToTextFile() + { + var provider = new TestFileLoggerProvider(TempPath); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine + + "2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt"))); + } + + [Fact] + public async Task RollsTextFile() + { + var provider = new TestFileLoggerProvider(TempPath); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt"))); + + Assert.Equal( + "2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt"))); + } + + [Fact] + public async Task RespectsMaxFileCount() + { + Directory.CreateDirectory(TempPath); + File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text"); + + var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + var timestamp = _timestampOne; + + for (int i = 0; i < 10; i++) + { + logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + timestamp = timestamp.AddDays(1); + } + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + var actualFiles = new DirectoryInfo(TempPath) + .GetFiles() + .Select(f => f.Name) + .OrderBy(f => f) + .ToArray(); + + Assert.Equal(6, actualFiles.Length); + Assert.Equal(new[] { + "LogFile.20160509.txt", + "LogFile.20160510.txt", + "LogFile.20160511.txt", + "LogFile.20160512.txt", + "LogFile.20160513.txt", + "randomFile.txt" + }, actualFiles); + } + } +} diff --git a/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs new file mode 100644 index 0000000000..cf8bede1a5 --- /dev/null +++ b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class LoggerBuilderExtensionsTests + { + private IWebAppContext _appContext; + + public LoggerBuilderExtensionsTests() + { + var contextMock = new Mock(); + contextMock.SetupGet(c => c.IsRunningInAzureWebApp).Returns(true); + contextMock.SetupGet(c => c.HomeFolder).Returns("."); + _appContext = contextMock.Object; + } + + [Fact] + public void BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + var count = serviceCollection.Count; + + Assert.NotEqual(0, count); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + Assert.Equal(count, serviceCollection.Count); + } + + [Fact] + public void BuilderExtensionAddsConfigurationChangeTokenSource() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build())); + + // Tracking for main configuration + Assert.Equal(1, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource))); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + // Make sure we add another config change token for azure diagnostic configuration + Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource))); + } + + [Fact] + public void BuilderExtensionAddsIConfigureOptions() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build())); + + // Tracking for main configuration + Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions))); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions))); + } + + [Fact] + public void LoggerProviderIsResolvable() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetService(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/ManualIntervalControl.cs b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs new file mode 100644 index 0000000000..29cc883a28 --- /dev/null +++ b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class ManualIntervalControl + { + + private TaskCompletionSource _pauseCompletionSource = new TaskCompletionSource(); + private TaskCompletionSource _resumeCompletionSource; + + public Task Pause => _pauseCompletionSource.Task; + + public void Resume() + { + _pauseCompletionSource = new TaskCompletionSource(); + _resumeCompletionSource.SetResult(null); + } + + public async Task IntervalAsync() + { + _resumeCompletionSource = new TaskCompletionSource(); + _pauseCompletionSource.SetResult(null); + + await _resumeCompletionSource.Task; + } + } +} \ No newline at end of file diff --git a/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj b/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj new file mode 100644 index 0000000000..7365c79076 --- /dev/null +++ b/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj @@ -0,0 +1,28 @@ + + + + $(DefaultNetCoreTargetFramework);net472 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.cs b/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.cs new file mode 100644 index 0000000000..fbc531c26d --- /dev/null +++ b/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class OptionsWrapperMonitor : IOptionsMonitor + { + public OptionsWrapperMonitor(T currentValue) + { + CurrentValue = currentValue; + } + + public IDisposable OnChange(Action listener) + { + return null; + } + + public T Get(string name) => CurrentValue; + + public T CurrentValue { get; } + } +} \ No newline at end of file diff --git a/src/Logging.AzureAppServices/test/TestBlobSink.cs b/src/Logging.AzureAppServices/test/TestBlobSink.cs new file mode 100644 index 0000000000..4b9ec445be --- /dev/null +++ b/src/Logging.AzureAppServices/test/TestBlobSink.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class TestBlobSink : BlobLoggerProvider + { + internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestBlobSink(Func blobReferenceFactory) : base( + new OptionsWrapperMonitor(new AzureBlobLoggerOptions() + { + ApplicationInstanceId = "42", + ApplicationName = "appname", + BlobName = "filename", + IsEnabled = true + }), + blobReferenceFactory) + { + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs b/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs new file mode 100644 index 0000000000..60fbb88bd8 --- /dev/null +++ b/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class TestFileLoggerProvider : FileLoggerProvider + { + internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestFileLoggerProvider( + string path, + string fileName = "LogFile.", + int maxFileSize = 32_000, + int maxRetainedFiles = 100) + : base(new OptionsWrapperMonitor(new AzureFileLoggerOptions() + { + LogDirectory = path, + FileName = fileName, + FileSizeLimit = maxFileSize, + RetainedFileCountLimit = maxRetainedFiles, + IsEnabled = true + })) + { + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.cs b/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.cs new file mode 100644 index 0000000000..f933b9f2fa --- /dev/null +++ b/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class WebConfigurationLevelSwitchTests + { + [Theory] + [InlineData("Error", LogLevel.Error)] + [InlineData("Warning", LogLevel.Warning)] + [InlineData("Information", LogLevel.Information)] + [InlineData("Verbose", LogLevel.Trace)] + [InlineData("ABCD", LogLevel.None)] + public void AddsRuleWithCorrectLevel(string levelValue, LogLevel expectedLevel) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection( + new[] + { + new KeyValuePair("levelKey", levelValue), + }) + .Build(); + + var levelSwitcher = new ConfigurationBasedLevelSwitcher(configuration, typeof(TestFileLoggerProvider), "levelKey"); + + var filterConfiguration = new LoggerFilterOptions(); + levelSwitcher.Configure(filterConfiguration); + + Assert.Equal(1, filterConfiguration.Rules.Count); + + var rule = filterConfiguration.Rules[0]; + Assert.Equal(typeof(TestFileLoggerProvider).FullName, rule.ProviderName); + Assert.Equal(expectedLevel, rule.LogLevel); + } + } +} diff --git a/src/Middleware/Session/samples/SessionSample.csproj b/src/Middleware/Session/samples/SessionSample.csproj index c1d8559a47..3beef6fa7b 100644 --- a/src/Middleware/Session/samples/SessionSample.csproj +++ b/src/Middleware/Session/samples/SessionSample.csproj @@ -9,8 +9,6 @@ - - diff --git a/src/Middleware/Session/samples/Startup.cs b/src/Middleware/Session/samples/Startup.cs index c776d8e2ff..9bc4265daf 100644 --- a/src/Middleware/Session/samples/Startup.cs +++ b/src/Middleware/Session/samples/Startup.cs @@ -21,7 +21,8 @@ namespace SessionSample // Uncomment the following line to use the in-memory implementation of IDistributedCache services.AddDistributedMemoryCache(); - // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache. + // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache + // and add a PackageReference to Microsoft.Extensions.Caching.SqlServer in the .csrpoj. // Note that this would require setting up the session state database. //services.AddDistributedSqlServerCache(o => //{ @@ -30,7 +31,8 @@ namespace SessionSample // o.TableName = "Sessions"; //}); - // Uncomment the following line to use the Redis implementation of IDistributedCache. + // Uncomment the following line to use the Redis implementation of IDistributedCache + // and add a PackageReference to Microsoft.Extensions.Caching.StackExchangeRedis in the .csrpoj. // This will override any previously registered IDistributedCache service. //services.AddStackExchangeRedisCache(o => //{ diff --git a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs index 8497ae2e6c..a198f9b799 100644 --- a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs +++ b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs @@ -81,7 +81,7 @@ namespace Templates.Test } } - [ConditionalTheory] + [ConditionalTheory(Skip = "See: https://github.com/dotnet/aspnetcore/issues/20520")] [InlineData(true)] [InlineData(false)] [SkipOnHelix("ef restore no worky")] diff --git a/src/ProjectTemplates/test/GrpcTemplateTest.cs b/src/ProjectTemplates/test/GrpcTemplateTest.cs index 707ffb9034..371c0022d3 100644 --- a/src/ProjectTemplates/test/GrpcTemplateTest.cs +++ b/src/ProjectTemplates/test/GrpcTemplateTest.cs @@ -25,7 +25,7 @@ namespace Templates.Test public ProjectFactoryFixture ProjectFactory { get; } public ITestOutputHelper Output { get; } - [ConditionalFact(Skip = "This test run for over an hour")] + [ConditionalFact] [SkipOnHelix("Not supported queues", Queues = "Windows.7.Amd64;Windows.7.Amd64.Open;OSX.1014.Amd64;OSX.1014.Amd64.Open")] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] public async Task GrpcTemplate() diff --git a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs index d90eb24713..71afbd842a 100644 --- a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs +++ b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -30,6 +31,9 @@ namespace Templates.Test.Helpers private readonly HttpClient _httpClient; private readonly ITestOutputHelper _output; + private string _certificatePath; + private string _certificatePassword = Guid.NewGuid().ToString(); + internal readonly Uri ListeningUri; internal ProcessEx Process { get; } @@ -48,12 +52,14 @@ namespace Templates.Test.Helpers AllowAutoRedirect = true, UseCookies = true, CookieContainer = new CookieContainer(), - ServerCertificateCustomValidationCallback = (m, c, ch, p) => true, + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, }) { Timeout = TimeSpan.FromMinutes(2) }; + _certificatePath = Path.Combine(workingDirectory, $"{Guid.NewGuid()}.pfx"); + EnsureDevelopmentCertificates(); output.WriteLine("Running ASP.NET application..."); @@ -62,7 +68,13 @@ namespace Templates.Test.Helpers logger?.LogInformation($"AspNetProcess - process: {DotNetMuxer.MuxerPathOrDefault()} arguments: {arguments}"); - Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: environmentVariables); + var finalEnvironmentVariables = new Dictionary(environmentVariables) + { + ["ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PATH"] = _certificatePath, + ["ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PASSWORD"] = _certificatePassword + }; + + Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables); logger?.LogInformation("AspNetProcess - process started"); @@ -74,10 +86,12 @@ namespace Templates.Test.Helpers } } - internal static void EnsureDevelopmentCertificates() + internal void EnsureDevelopmentCertificates() { var now = DateTimeOffset.Now; - new CertificateManager().EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1)); + var manager = new CertificateManager(); + var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), "CN=localhost"); + manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword); } public void VisitInBrowser(IWebDriver driver) diff --git a/src/ProjectTemplates/test/IdentityUIPackageTest.cs b/src/ProjectTemplates/test/IdentityUIPackageTest.cs index da766229a6..b89c19b61a 100644 --- a/src/ProjectTemplates/test/IdentityUIPackageTest.cs +++ b/src/ProjectTemplates/test/IdentityUIPackageTest.cs @@ -118,7 +118,7 @@ namespace Templates.Test "Identity/lib/jquery-validation-unobtrusive/LICENSE.txt", }; - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [MemberData(nameof(MSBuildIdentityUIPackageOptions))] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs index d881ffcdd0..ff60a16bd8 100644 --- a/src/ProjectTemplates/test/MvcTemplateTest.cs +++ b/src/ProjectTemplates/test/MvcTemplateTest.cs @@ -104,7 +104,7 @@ namespace Templates.Test } } - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [InlineData(true)] [InlineData(false)] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] diff --git a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs index f7727fbb5a..10370a3573 100644 --- a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs +++ b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs @@ -94,7 +94,7 @@ namespace Templates.Test } } - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [InlineData(false)] [InlineData(true)] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] diff --git a/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs b/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs index d4a1ff1756..9e591ff67d 100644 --- a/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs +++ b/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs @@ -23,7 +23,7 @@ namespace Templates.Test.SpaTemplateTest => SpaTemplateImplAsync("reactnoauth", "react", useLocalDb: false, usesAuth: false); [QuarantinedTest] - [ConditionalFact(Skip="This test run for over an hour")] + [ConditionalFact] [SkipOnHelix("selenium")] public Task ReactTemplate_IndividualAuth_NetCore() => SpaTemplateImplAsync("reactindividual", "react", useLocalDb: false, usesAuth: true); diff --git a/src/ProjectTemplates/test/WorkerTemplateTest.cs b/src/ProjectTemplates/test/WorkerTemplateTest.cs index 09f444811c..1bc2d8d5b3 100644 --- a/src/ProjectTemplates/test/WorkerTemplateTest.cs +++ b/src/ProjectTemplates/test/WorkerTemplateTest.cs @@ -21,7 +21,7 @@ namespace Templates.Test public ProjectFactoryFixture ProjectFactory { get; } public ITestOutputHelper Output { get; } - [Fact(Skip = "This test run for over an hour")] + [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] public async Task WorkerTemplateAsync() { diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs index b0847f84a1..5503c2b4fa 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests } catch (Exception ex) { - Logger.LogError($"Certificate is invalid. Issuer name: {cert.Issuer}"); + Logger.LogError($"Certificate is invalid. Issuer name: {cert?.Issuer}"); using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) { Logger.LogError($"List of current certificates in root store:"); diff --git a/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj b/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj index ced51607d8..e4ff5df0cc 100644 --- a/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj +++ b/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj @@ -7,8 +7,8 @@ + - diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj index fabcb2938a..7e319b26df 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj +++ b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj @@ -7,6 +7,7 @@ InProcessWebSite InProcessNewShimWebSite FORWARDCOMPAT + false diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 539c386efa..6efacd2e74 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -547,6 +547,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { try { + // We run the request processing loop in a seperate async method so per connection + // exception handling doesn't complicate the generated asm for the loop. await ProcessRequests(application); } catch (BadHttpRequestException ex) @@ -624,91 +626,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http InitializeBodyControl(messageBody); - var context = application.CreateContext(this); - - try - { - KestrelEventSource.Log.RequestStart(this); - - // Run the application code for this request - await application.ProcessRequestAsync(context); - - // Trigger OnStarting if it hasn't been called yet and the app hasn't - // already failed. If an OnStarting callback throws we can go through - // our normal error handling in ProduceEnd. - // https://github.com/aspnet/KestrelHttpServer/issues/43 - if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0) - { - await FireOnStarting(); - } - - if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException)) - { - ReportApplicationError(lengthException); - } - } - catch (BadHttpRequestException ex) - { - // Capture BadHttpRequestException for further processing - // This has to be caught here so StatusCode is set properly before disposing the HttpContext - // (DisposeContext logs StatusCode). - SetBadRequestState(ex); - ReportApplicationError(ex); - } - catch (Exception ex) - { - ReportApplicationError(ex); - } - - KestrelEventSource.Log.RequestStop(this); - - // At this point all user code that needs use to the request or response streams has completed. - // Using these streams in the OnCompleted callback is not allowed. - try - { - await _bodyControl.StopAsync(); - } - catch (Exception ex) - { - // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing - // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception, - // but this scenario generally indicates an app bug, so I don't want to risk not logging it. - ReportApplicationError(ex); - } - - // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. - if (_requestRejectedException == null) - { - if (!_connectionAborted) - { - // Call ProduceEnd() before consuming the rest of the request body to prevent - // delaying clients waiting for the chunk terminator: - // - // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663 - // - // This also prevents the 100 Continue response from being sent if the app - // never tried to read the body. - // https://github.com/aspnet/KestrelHttpServer/issues/2102 - // - // ProduceEnd() must be called before _application.DisposeContext(), to ensure - // HttpContext.Response.StatusCode is correctly set when - // IHttpContextFactory.Dispose(HttpContext) is called. - await ProduceEnd(); - } - else if (!HasResponseStarted) - { - // If the request was aborted and no response was sent, there's no - // meaningful status code to log. - StatusCode = 0; - } - } - - if (_onCompleted?.Count > 0) - { - await FireOnCompleted(); - } - - application.DisposeContext(context, _applicationException); + // We run user controlled request processing in a seperate async method + // so any changes made to ExecutionContext are undone when it returns and + // each request starts with a fresh ExecutionContext state. + await ProcessRequest(application); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty) @@ -723,6 +644,95 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + private async ValueTask ProcessRequest(IHttpApplication application) + { + var context = application.CreateContext(this); + + try + { + KestrelEventSource.Log.RequestStart(this); + + // Run the application code for this request + await application.ProcessRequestAsync(context); + + // Trigger OnStarting if it hasn't been called yet and the app hasn't + // already failed. If an OnStarting callback throws we can go through + // our normal error handling in ProduceEnd. + // https://github.com/aspnet/KestrelHttpServer/issues/43 + if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0) + { + await FireOnStarting(); + } + + if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException)) + { + ReportApplicationError(lengthException); + } + } + catch (BadHttpRequestException ex) + { + // Capture BadHttpRequestException for further processing + // This has to be caught here so StatusCode is set properly before disposing the HttpContext + // (DisposeContext logs StatusCode). + SetBadRequestState(ex); + ReportApplicationError(ex); + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + + KestrelEventSource.Log.RequestStop(this); + + // At this point all user code that needs use to the request or response streams has completed. + // Using these streams in the OnCompleted callback is not allowed. + try + { + await _bodyControl.StopAsync(); + } + catch (Exception ex) + { + // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing + // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception, + // but this scenario generally indicates an app bug, so I don't want to risk not logging it. + ReportApplicationError(ex); + } + + // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. + if (_requestRejectedException == null) + { + if (!_connectionAborted) + { + // Call ProduceEnd() before consuming the rest of the request body to prevent + // delaying clients waiting for the chunk terminator: + // + // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663 + // + // This also prevents the 100 Continue response from being sent if the app + // never tried to read the body. + // https://github.com/aspnet/KestrelHttpServer/issues/2102 + // + // ProduceEnd() must be called before _application.DisposeContext(), to ensure + // HttpContext.Response.StatusCode is correctly set when + // IHttpContextFactory.Dispose(HttpContext) is called. + await ProduceEnd(); + } + else if (!HasResponseStarted) + { + // If the request was aborted and no response was sent, there's no + // meaningful status code to log. + StatusCode = 0; + } + } + + if (_onCompleted?.Count > 0) + { + await FireOnCompleted(); + } + + application.DisposeContext(context, _applicationException); + } + public void OnStarting(Func callback, object state) { if (HasResponseStarted) @@ -749,108 +759,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected Task FireOnStarting() { var onStarting = _onStarting; - - if (onStarting == null || onStarting.Count == 0) + if (onStarting?.Count > 0) { - return Task.CompletedTask; - } - else - { - return FireOnStartingMayAwait(onStarting); - } - } - - private Task FireOnStartingMayAwait(Stack, object>> onStarting) - { - try - { - while (onStarting.TryPop(out var entry)) - { - var task = entry.Key.Invoke(entry.Value); - if (!task.IsCompletedSuccessfully) - { - return FireOnStartingAwaited(task, onStarting); - } - } - } - catch (Exception ex) - { - ReportApplicationError(ex); + return ProcessEvents(this, onStarting); } return Task.CompletedTask; - } - private async Task FireOnStartingAwaited(Task currentTask, Stack, object>> onStarting) - { - try + static async Task ProcessEvents(HttpProtocol protocol, Stack, object>> events) { - await currentTask; - - while (onStarting.TryPop(out var entry)) + // Try/Catch is outside the loop as any error that occurs is before the request starts. + // So we want to report it as an ApplicationError to fail the request and not process more events. + try { - await entry.Key.Invoke(entry.Value); + while (events.TryPop(out var entry)) + { + await entry.Key.Invoke(entry.Value); + } + } + catch (Exception ex) + { + protocol.ReportApplicationError(ex); } - } - catch (Exception ex) - { - ReportApplicationError(ex); } } protected Task FireOnCompleted() { var onCompleted = _onCompleted; - - if (onCompleted == null || onCompleted.Count == 0) + if (onCompleted?.Count > 0) { - return Task.CompletedTask; - } - - return FireOnCompletedMayAwait(onCompleted); - } - - private Task FireOnCompletedMayAwait(Stack, object>> onCompleted) - { - while (onCompleted.TryPop(out var entry)) - { - try - { - var task = entry.Key.Invoke(entry.Value); - if (!task.IsCompletedSuccessfully) - { - return FireOnCompletedAwaited(task, onCompleted); - } - } - catch (Exception ex) - { - ReportApplicationError(ex); - } + return ProcessEvents(this, onCompleted); } return Task.CompletedTask; - } - private async Task FireOnCompletedAwaited(Task currentTask, Stack, object>> onCompleted) - { - try + static async Task ProcessEvents(HttpProtocol protocol, Stack, object>> events) { - await currentTask; - } - catch (Exception ex) - { - Log.ApplicationError(ConnectionId, TraceIdentifier, ex); - } - - while (onCompleted.TryPop(out var entry)) - { - try + // Try/Catch is inside the loop as any error that occurs is after the request has finished. + // So we will just log it and keep processing the events, as the completion has already happened. + while (events.TryPop(out var entry)) { - await entry.Key.Invoke(entry.Value); - } - catch (Exception ex) - { - Log.ApplicationError(ConnectionId, TraceIdentifier, ex); + try + { + await entry.Key.Invoke(entry.Value); + } + catch (Exception ex) + { + protocol.Log.ApplicationError(protocol.ConnectionId, protocol.TraceIdentifier, ex); + } } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index 0f95f70ef7..cc30a8ee66 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -41,10 +41,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private bool _suffixSent; private bool _streamEnded; private bool _writerComplete; - private bool _disposed; // Internal for testing internal ValueTask _dataWriteProcessingTask; + internal bool _disposed; /// The core logic for the IValueTaskSource implementation. private ManualResetValueTaskSourceCore _responseCompleteTaskSource = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; // mutable struct, do not make this readonly diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs index 1c141dd236..88512a19df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs @@ -37,6 +37,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return true; } + public bool TryPeek(out Http2Stream result) + { + int size = _size - 1; + Http2StreamAsValueType[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + result = array[size]; + return true; + } + // Pushes an item to the top of the stack. public void Push(Http2Stream item) { diff --git a/src/Servers/Kestrel/Directory.Build.props b/src/Servers/Kestrel/Directory.Build.props index 05b71894da..1b0e999039 100644 --- a/src/Servers/Kestrel/Directory.Build.props +++ b/src/Servers/Kestrel/Directory.Build.props @@ -20,7 +20,5 @@ - - diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs index 3960ebe388..8a38f97709 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs @@ -26,6 +26,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } } + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void JsonTechEmpower() + { + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + { + InsertData(RequestParsingData.JsonTechEmpowerRequest); + ParseData(); + } + } + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] public void LiveAspNet() { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs index 5c496960bb..7b50a6fba0 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Linq; @@ -19,6 +19,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance "Connection: keep-alive\r\n" + "\r\n"; + private const string _jsonTechEmpowerRequest = + "GET /json HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Accept: Accept:application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" + + "Connection: keep-alive\r\n" + + "\r\n"; + // edge-casey - client's don't normally send this private const string _plaintextAbsoluteUriRequest = "GET http://localhost/plaintext HTTP/1.1\r\n" + @@ -59,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining))); public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest); + public static readonly byte[] JsonTechEmpowerRequest = Encoding.ASCII.GetBytes(_jsonTechEmpowerRequest); + public static readonly byte[] PlaintextAbsoluteUriRequest = Encoding.ASCII.GetBytes(_plaintextAbsoluteUriRequest); public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining))); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 1a7bd27c24..d5a40a1127 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -414,6 +414,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + // Get the in progress stream + var stream = _connection._streams[1]; + appDelegateTcs.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, @@ -421,15 +424,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); - // Stream has been returned to the pool - Assert.Equal(1, _connection.StreamPool.Count); + await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout(); + Assert.True(_connection.StreamPool.TryPeek(out var pooledStream)); + Assert.Equal(stream, pooledStream); appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await StartStreamAsync(3, _browserRequestHeaders, endStream: true); @@ -444,17 +442,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); - // Stream was reused and returned to the pool - Assert.Equal(1, _connection.StreamPool.Count); + await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout(); + Assert.True(_connection.StreamPool.TryPeek(out pooledStream)); + Assert.Equal(stream, pooledStream); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + async Task PingUntilStreamPooled(int expectedCount) + { + do + { + // Ping will trigger the stream to be returned to the pool so we can assert it + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + } while (_connection.StreamPool.Count != expectedCount); + } } [Fact] @@ -486,12 +492,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 1); await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, null); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); + await PingUntilStreamDisposed(stream).DefaultTimeout(); // Stream is not returned to the pool Assert.Equal(0, _connection.StreamPool.Count); @@ -500,6 +501,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await output._dataWriteProcessingTask.DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + async Task PingUntilStreamDisposed(Http2Stream stream) + { + var output = (Http2OutputProducer)stream.Output; + + do + { + // Ping will trigger the stream to be returned to the pool so we can assert it + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + } while (!output._disposed); + } } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index dafbd6eca7..178a464b44 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -285,6 +285,264 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } + [Fact] + public async Task ExecutionContextMutationsOfValueTypeDoNotLeakAcrossRequestsOnSameConnection() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value++; + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value++; + return Task.CompletedTask; + }); + + local.Value++; + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfReferenceTypeDoNotLeakAcrossRequestsOnSameConnection() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + Assert.Null(local.Value); + local.Value = new IntAsClass(); + + var value = local.Value.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value.Value++; + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value.Value++; + return Task.CompletedTask; + }); + + local.Value.Value++; + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + [Fact] + public async Task ExecutionContextMutationsDoNotLeakAcrossAwaits() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(async () => + { + local.Value++; + Assert.Equal(1, local.Value); + }); + + context.Response.OnCompleted(async () => + { + local.Value++; + Assert.Equal(1, local.Value); + }); + + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(async () => + { + local.Value++; + Assert.Equal(2, local.Value); + }); + + context.Response.OnCompleted(async () => + { + local.Value++; + Assert.Equal(2, local.Value); + }); + + local.Value++; + Assert.Equal(1, local.Value); + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value++; + Assert.Equal(2, local.Value); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfReferenceTypeFlowThroughAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + Assert.Null(local.Value); + local.Value = new IntAsClass(); + + var value = local.Value.Value; + Assert.Equal(0, value); // Start + + context.Response.OnStarting(async () => + { + local.Value.Value++; + Assert.Equal(2, local.Value.Value); // Second + }); + + context.Response.OnCompleted(async () => + { + local.Value.Value++; + Assert.Equal(4, local.Value.Value); // Fourth + }); + + local.Value.Value++; + Assert.Equal(1, local.Value.Value); // First + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value.Value++; + Assert.Equal(3, local.Value.Value); // Third + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + [Fact] + public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfNonAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value++; + Assert.Equal(2, local.Value); + + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value++; + Assert.Equal(2, local.Value); + + return Task.CompletedTask; + }); + + local.Value++; + Assert.Equal(1, local.Value); + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value++; + Assert.Equal(2, local.Value); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + private static async Task TestAsyncLocalValues(TestServiceContext testContext, TestServer server) + { + using var connection = server.CreateConnection(); + + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 1", + "", + "0"); + + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 1", + "", + "0"); + } + [Fact] public async Task AppCanSetTraceIdentifier() { @@ -1803,5 +2061,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; + + private class IntAsClass + { + public int Value; + } } } diff --git a/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs index a26fb7b133..3301687ee4 100644 --- a/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs +++ b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs @@ -82,10 +82,10 @@ namespace Microsoft.Extensions.Hosting.Tests [Fact] public void CreateHostBuilderPattern_CanFindHostBuilder() { - var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); Assert.NotNull(factory); - Assert.IsAssignableFrom(factory(Array.Empty())); + Assert.IsAssignableFrom(factory(Array.Empty())); } [Fact] @@ -100,7 +100,7 @@ namespace Microsoft.Extensions.Hosting.Tests [Fact] public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() { - var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); Assert.Null(factory); } diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index bc6eb225c1..c1db74970e 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/SignalR/Directory.Build.props b/src/SignalR/Directory.Build.props index 27a3751cd8..74b4c7adee 100644 --- a/src/SignalR/Directory.Build.props +++ b/src/SignalR/Directory.Build.props @@ -21,7 +21,6 @@ PreserveNewest - diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs index d6f1a97bbe..c6e2878f2f 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs @@ -235,7 +235,7 @@ namespace Microsoft.AspNetCore.SignalR.Client private async Task StartAsyncInner(CancellationToken cancellationToken = default) { - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: cancellationToken); try { if (!_state.TryChangeState(HubConnectionState.Disconnected, HubConnectionState.Connecting)) @@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.SignalR.Client // Potentially wait for StartAsync to finish, and block a new StartAsync from // starting until we've finished stopping. - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); // Ensure that ReconnectingState.ReconnectTask is not accessed outside of the lock. var reconnectTask = _state.ReconnectTask; @@ -478,7 +478,7 @@ namespace Microsoft.AspNetCore.SignalR.Client // The StopCts should prevent the HubConnection from restarting until it is reset. _state.ReleaseConnectionLock(); await reconnectTask; - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); } ConnectionState connectionState; @@ -574,7 +574,7 @@ namespace Microsoft.AspNetCore.SignalR.Client async Task OnStreamCanceled(InvocationRequest irq) { // We need to take the connection lock in order to ensure we a) have a connection and b) are the only one accessing the write end of the pipe. - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); try { if (_state.CurrentConnectionStateUnsynchronized != null) @@ -601,7 +601,7 @@ namespace Microsoft.AspNetCore.SignalR.Client var readers = default(Dictionary); CheckDisposed(); - var connectionState = await _state.WaitForActiveConnectionAsync(nameof(StreamAsChannelCoreAsync)); + var connectionState = await _state.WaitForActiveConnectionAsync(nameof(StreamAsChannelCoreAsync), token: cancellationToken); ChannelReader channel; try @@ -704,7 +704,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { while (!tokenSource.Token.IsCancellationRequested && reader.TryRead(out var item)) { - await SendWithLock(connectionState, new StreamItemMessage(streamId, item)); + await SendWithLock(connectionState, new StreamItemMessage(streamId, item), tokenSource.Token); Log.SendingStreamItem(_logger, streamId); } } @@ -722,7 +722,7 @@ namespace Microsoft.AspNetCore.SignalR.Client await foreach (var streamValue in streamValues) { - await SendWithLock(connectionState, new StreamItemMessage(streamId, streamValue)); + await SendWithLock(connectionState, new StreamItemMessage(streamId, streamValue), tokenSource.Token); Log.SendingStreamItem(_logger, streamId); } } @@ -750,7 +750,9 @@ namespace Microsoft.AspNetCore.SignalR.Client Log.CompletingStream(_logger, streamId); - await SendWithLock(connectionState, CompletionMessage.WithError(streamId, responseError), cts.Token); + // Don't use cancellation token here + // this is triggered by a cancellation token to tell the server that the client is done streaming + await SendWithLock(connectionState, CompletionMessage.WithError(streamId, responseError), cancellationToken: default); } private async Task InvokeCoreAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken) @@ -758,7 +760,7 @@ namespace Microsoft.AspNetCore.SignalR.Client var readers = default(Dictionary); CheckDisposed(); - var connectionState = await _state.WaitForActiveConnectionAsync(nameof(InvokeCoreAsync)); + var connectionState = await _state.WaitForActiveConnectionAsync(nameof(InvokeCoreAsync), token: cancellationToken); Task invocationTask; try @@ -853,7 +855,7 @@ namespace Microsoft.AspNetCore.SignalR.Client var readers = default(Dictionary); CheckDisposed(); - var connectionState = await _state.WaitForActiveConnectionAsync(nameof(SendCoreAsync)); + var connectionState = await _state.WaitForActiveConnectionAsync(nameof(SendCoreAsync), token: cancellationToken); try { CheckDisposed(); @@ -872,10 +874,10 @@ namespace Microsoft.AspNetCore.SignalR.Client } } - private async Task SendWithLock(ConnectionState expectedConnectionState, HubMessage message, CancellationToken cancellationToken = default, [CallerMemberName] string callerName = "") + private async Task SendWithLock(ConnectionState expectedConnectionState, HubMessage message, CancellationToken cancellationToken, [CallerMemberName] string callerName = "") { CheckDisposed(); - var connectionState = await _state.WaitForActiveConnectionAsync(callerName); + var connectionState = await _state.WaitForActiveConnectionAsync(callerName, token: cancellationToken); try { CheckDisposed(); @@ -1245,7 +1247,7 @@ namespace Microsoft.AspNetCore.SignalR.Client private async Task HandleConnectionClose(ConnectionState connectionState) { // Clear the connectionState field - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); try { SafeAssert(ReferenceEquals(_state.CurrentConnectionStateUnsynchronized, connectionState), @@ -1363,7 +1365,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { Log.ReconnectingStoppedDuringRetryDelay(_logger); - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); try { _state.ChangeState(HubConnectionState.Reconnecting, HubConnectionState.Disconnected); @@ -1378,7 +1380,7 @@ namespace Microsoft.AspNetCore.SignalR.Client return; } - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); try { SafeAssert(ReferenceEquals(_state.CurrentConnectionStateUnsynchronized, null), @@ -1417,7 +1419,7 @@ namespace Microsoft.AspNetCore.SignalR.Client nextRetryDelay = GetNextRetryDelay(previousReconnectAttempts++, DateTime.UtcNow - reconnectStartTime, retryReason); } - await _state.WaitConnectionLockAsync(); + await _state.WaitConnectionLockAsync(token: default); try { SafeAssert(ReferenceEquals(_state.CurrentConnectionStateUnsynchronized, null), @@ -1956,10 +1958,10 @@ namespace Microsoft.AspNetCore.SignalR.Client SafeAssert(CurrentConnectionStateUnsynchronized != null, "We don't have a connection!", memberName, fileName, lineNumber); } - public Task WaitConnectionLockAsync([CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) + public Task WaitConnectionLockAsync(CancellationToken token, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) { Log.WaitingOnConnectionLock(_logger, memberName, filePath, lineNumber); - return _connectionLock.WaitAsync(); + return _connectionLock.WaitAsync(token); } public bool TryAcquireConnectionLock() @@ -1968,9 +1970,9 @@ namespace Microsoft.AspNetCore.SignalR.Client } // Don't call this method in a try/finally that releases the lock since we're also potentially releasing the connection lock here. - public async Task WaitForActiveConnectionAsync(string methodName, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) + public async Task WaitForActiveConnectionAsync(string methodName, CancellationToken token, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) { - await WaitConnectionLockAsync(methodName); + await WaitConnectionLockAsync(token, methodName); if (CurrentConnectionStateUnsynchronized == null || CurrentConnectionStateUnsynchronized.Stopping) { diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index 2220685e82..afe4254ded 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -1335,6 +1335,102 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + [Theory] + [QuarantinedTest] + [MemberData(nameof(HubProtocolsList))] + public async Task ServerLogsErrorIfClientInvokeCannotBeSerialized(string protocolName) + { + // Just to help sanity check that the right exception is thrown + var exceptionSubstring = protocolName switch + { + "json" => "A possible object cycle was detected.", + "newtonsoft-json" => "A possible object cycle was detected.", + "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.", + var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"), + }; + + var protocol = HubProtocols[protocolName]; + using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage")) + { + var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.WebSockets, protocol, LoggerFactory); + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; }; + try + { + await connection.StartAsync().OrTimeout(); + + var result = connection.InvokeAsync(nameof(TestHub.CallWithUnserializableObject)); + + // The connection should close. + Assert.Null(await closedTcs.Task.OrTimeout()); + + await Assert.ThrowsAsync(() => result).OrTimeout(); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().OrTimeout(); + } + + var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage"); + Assert.NotNull(errorLog); + Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message); + Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel); + } + } + + [Theory] + [QuarantinedTest] + [MemberData(nameof(HubProtocolsList))] + public async Task ServerLogsErrorIfReturnValueCannotBeSerialized(string protocolName) + { + // Just to help sanity check that the right exception is thrown + var exceptionSubstring = protocolName switch + { + "json" => "A possible object cycle was detected.", + "newtonsoft-json" => "A possible object cycle was detected.", + "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.", + var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"), + }; + + var protocol = HubProtocols[protocolName]; + using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage")) + { + var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.LongPolling, protocol, LoggerFactory); + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; }; + try + { + await connection.StartAsync().OrTimeout(); + + var result = connection.InvokeAsync(nameof(TestHub.GetUnserializableObject)).OrTimeout(); + + // The connection should close. + Assert.Null(await closedTcs.Task.OrTimeout()); + + await Assert.ThrowsAsync(() => result).OrTimeout(); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().OrTimeout(); + } + + var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage"); + Assert.NotNull(errorLog); + Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message); + Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel); + } + } + [Fact] public async Task RandomGenericIsNotTreatedAsStream() { diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs index 01ce4198c4..90328a6b57 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs @@ -95,6 +95,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { return Context.Features.Get().TransportType.ToString(); } + + public async Task CallWithUnserializableObject() + { + await Clients.All.SendAsync("Foo", Unserializable.Create()); + } + + public Unserializable GetUnserializableObject() + { + return Unserializable.Create(); + } + + public class Unserializable + { + public Unserializable Child { get; private set; } + + private Unserializable() + { + } + + internal static Unserializable Create() + { + // Loops throw off every serializer ;). + var o = new Unserializable(); + o.Child = o; + return o; + } + } } public class DynamicTestHub : DynamicHub diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs index fa95fbc83b..3b16171787 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs @@ -443,7 +443,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests })), async (connection) => { - // We aggregate failures that happen when we start the transport. The operation cancelled exception will + // We aggregate failures that happen when we start the transport. The operation canceled exception will // be an inner exception. var ex = await Assert.ThrowsAsync(async () => await connection.StartAsync(cts.Token)).OrTimeout(); Assert.Equal(3, ex.InnerExceptions.Count); @@ -454,6 +454,29 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task CanceledCancellationTokenPassedToStartThrows() + { + using (StartVerifiableLog()) + { + bool transportStartCalled = false; + var httpHandler = new TestHttpMessageHandler(); + + await WithConnectionAsync( + CreateConnection(httpHandler, + transport: new TestTransport(onTransportStart: () => { + transportStartCalled = true; + return Task.CompletedTask; + })), + async (connection) => + { + await Assert.ThrowsAsync(async () => await connection.StartAsync(new CancellationToken(canceled: true))).OrTimeout(); + }); + + Assert.False(transportStartCalled); + } + } + [Fact] public async Task SSECanBeCanceled() { diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs index f1d191ee8c..77f380602e 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs @@ -541,7 +541,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); try { - await Assert.ThrowsAsync(() => hubConnection.StartAsync(new CancellationToken(canceled: true))).OrTimeout(); + await Assert.ThrowsAsync(() => hubConnection.StartAsync(new CancellationToken(canceled: true))).OrTimeout(); Assert.False(onStartCalled); } finally diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs index 5c9f90211a..1a8d99e784 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs @@ -162,6 +162,98 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task PendingInvocationsAreCanceledWhenTokenTriggered() + { + using (StartVerifiableLog()) + { + var hubConnection = CreateHubConnection(new TestConnection(), loggerFactory: LoggerFactory); + + await hubConnection.StartAsync().OrTimeout(); + var cts = new CancellationTokenSource(); + var invokeTask = hubConnection.InvokeAsync("testMethod", cancellationToken: cts.Token).OrTimeout(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => await invokeTask); + } + } + + [Fact] + public async Task InvokeAsyncCanceledWhenPassedCanceledToken() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + + await hubConnection.StartAsync().OrTimeout(); + await Assert.ThrowsAsync(() => + hubConnection.InvokeAsync("testMethod", cancellationToken: new CancellationToken(canceled: true)).OrTimeout()); + + await hubConnection.StopAsync().OrTimeout(); + + // Assert that InvokeAsync didn't send a message + Assert.Null(await connection.ReadSentTextMessageAsync().OrTimeout()); + } + } + + [Fact] + public async Task SendAsyncCanceledWhenPassedCanceledToken() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + + await hubConnection.StartAsync().OrTimeout(); + await Assert.ThrowsAsync(() => + hubConnection.SendAsync("testMethod", cancellationToken: new CancellationToken(canceled: true)).OrTimeout()); + + await hubConnection.StopAsync().OrTimeout(); + + // Assert that SendAsync didn't send a message + Assert.Null(await connection.ReadSentTextMessageAsync().OrTimeout()); + } + } + + [Fact] + public async Task StreamAsChannelAsyncCanceledWhenPassedCanceledToken() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + + await hubConnection.StartAsync().OrTimeout(); + await Assert.ThrowsAsync(() => + hubConnection.StreamAsChannelAsync("testMethod", cancellationToken: new CancellationToken(canceled: true)).OrTimeout()); + + await hubConnection.StopAsync().OrTimeout(); + + // Assert that StreamAsChannelAsync didn't send a message + Assert.Null(await connection.ReadSentTextMessageAsync().OrTimeout()); + } + } + + [Fact] + public async Task StreamAsyncCanceledWhenPassedCanceledToken() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + + await hubConnection.StartAsync().OrTimeout(); + var result = hubConnection.StreamAsync("testMethod", cancellationToken: new CancellationToken(canceled: true)); + await Assert.ThrowsAsync(() => result.GetAsyncEnumerator().MoveNextAsync().OrTimeout()); + + await hubConnection.StopAsync().OrTimeout(); + + // Assert that StreamAsync didn't send a message + Assert.Null(await connection.ReadSentTextMessageAsync().OrTimeout()); + } + } + [Fact] public async Task ConnectionTerminatedIfServerTimeoutIntervalElapsesWithNoMessages() { @@ -362,7 +454,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] [LogLevel(LogLevel.Trace)] - public async Task UploadStreamCancelationSendsStreamComplete() + public async Task UploadStreamCancellationSendsStreamComplete() { using (StartVerifiableLog()) { diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index acfdce1010..1c2daa9cdf 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client return; } - await _connectionLock.WaitAsync(); + await _connectionLock.WaitAsync(cancellationToken); try { CheckDisposed(); diff --git a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs index 85025b6444..bd2ad8f788 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs +++ b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs @@ -9,24 +9,31 @@ namespace Microsoft.AspNetCore.SignalR { public class MessagePackHubProtocolOptions { - private IList _formatterResolvers; + private MessagePackSerializerOptions _messagePackSerializerOptions; - public IList FormatterResolvers + /// + /// Gets or sets the used internally by the . + /// If you override the default value, we strongly recommend that you set to by calling: + /// customMessagePackSerializerOptions = customMessagePackSerializerOptions.WithSecurity(MessagePackSecurity.UntrustedData) + /// If you modify the default options you must also assign the updated options back to the property: + /// options.SerializerOptions = options.SerializerOptions.WithResolver(new CustomResolver()); + /// + public MessagePackSerializerOptions SerializerOptions { get { - if (_formatterResolvers == null) + if (_messagePackSerializerOptions == null) { // The default set of resolvers trigger a static constructor that throws on AOT environments. // This gives users the chance to use an AOT friendly formatter. - _formatterResolvers = MessagePackHubProtocol.CreateDefaultFormatterResolvers(); + _messagePackSerializerOptions = MessagePackHubProtocol.CreateDefaultMessagePackSerializerOptions(); } - return _formatterResolvers; + return _messagePackSerializerOptions; } set { - _formatterResolvers = value; + _messagePackSerializerOptions = value; } } } diff --git a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs index e1a762937b..ffa814da22 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs +++ b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private const int NonVoidResult = 3; private readonly MessagePackSerializerOptions _msgPackSerializerOptions; + private static readonly string ProtocolName = "messagepack"; private static readonly int ProtocolVersion = 1; @@ -52,37 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// The options used to initialize the protocol. public MessagePackHubProtocol(IOptions options) { - var msgPackOptions = options.Value; - var resolver = SignalRResolver.Instance; - var hasCustomFormatterResolver = false; - - // if counts don't match then we know users customized resolvers so we set up the options with the provided resolvers - if (msgPackOptions.FormatterResolvers.Count != SignalRResolver.Resolvers.Count) - { - hasCustomFormatterResolver = true; - } - else - { - // Compare each "reference" in the FormatterResolvers IList<> against the default "SignalRResolver.Resolvers" IList<> - for (var i = 0; i < msgPackOptions.FormatterResolvers.Count; i++) - { - // check if the user customized the resolvers - if (msgPackOptions.FormatterResolvers[i] != SignalRResolver.Resolvers[i]) - { - hasCustomFormatterResolver = true; - break; - } - } - } - - if (hasCustomFormatterResolver) - { - resolver = CompositeResolver.Create(Array.Empty(), (IReadOnlyList)msgPackOptions.FormatterResolvers); - } - - _msgPackSerializerOptions = MessagePackSerializerOptions.Standard - .WithResolver(resolver) - .WithSecurity(MessagePackSecurity.UntrustedData); + _msgPackSerializerOptions = options.Value.SerializerOptions; } /// @@ -656,17 +627,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol } } - internal static List CreateDefaultFormatterResolvers() - { - // Copy to allow users to add/remove resolvers without changing the static SignalRResolver list - return new List(SignalRResolver.Resolvers); - } + internal static MessagePackSerializerOptions CreateDefaultMessagePackSerializerOptions() => + MessagePackSerializerOptions + .Standard + .WithResolver(SignalRResolver.Instance) + .WithSecurity(MessagePackSecurity.UntrustedData); internal class SignalRResolver : IFormatterResolver { public static readonly IFormatterResolver Instance = new SignalRResolver(); - public static readonly IList Resolvers = new IFormatterResolver[] + public static readonly IReadOnlyList Resolvers = new IFormatterResolver[] { DynamicEnumAsStringResolver.Instance, ContractlessStandardResolver.Instance, diff --git a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs index a02e4bb1de..631220c7de 100644 --- a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs +++ b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs @@ -84,6 +84,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests _logger = _loggerFactory.CreateLogger>(); } + public IList GetLogs() => _logSinkProvider.GetLogs(); + private async Task StartServerInner() { // We're using 127.0.0.1 instead of localhost to ensure that we use IPV4 across different OSes diff --git a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs index 9193044270..7c9d9f071c 100644 --- a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs +++ b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Testing; namespace Microsoft.AspNetCore.SignalR.Tests { // WriteContext, but with a timestamp... - internal class LogRecord + public class LogRecord { public DateTime Timestamp { get; } public WriteContext Write { get; } diff --git a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj index 9e69675720..782c8410b8 100644 --- a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj +++ b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj @@ -21,7 +21,6 @@ - diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index 91516701e4..fcd704c428 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -689,7 +689,7 @@ namespace Microsoft.AspNetCore.SignalR LoggerMessage.Define(LogLevel.Debug, new EventId(5, "HandshakeFailed"), "Failed connection handshake."); private static readonly Action _failedWritingMessage = - LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection."); + LoggerMessage.Define(LogLevel.Error, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection."); private static readonly Action _protocolVersionFailed = LoggerMessage.Define(LogLevel.Debug, new EventId(7, "ProtocolVersionFailed"), "Server does not support version {Version} of the {Protocol} protocol."); diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 4439cf3bb9..eb6d11a9dc 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using MessagePack; using MessagePack.Formatters; +using MessagePack.Resolvers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; @@ -2371,7 +2372,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests services.AddSignalR() .AddMessagePackProtocol(options => { - options.FormatterResolvers.Insert(0, new CustomFormatter()); + options.SerializerOptions = MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter(), options.SerializerOptions.Resolver)); }); }, LoggerFactory); @@ -3298,7 +3299,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests [Fact] public async Task ConnectionAbortedIfSendFailsWithProtocolError() { - using (StartVerifiableLog()) + using (StartVerifiableLog(write => write.EventId.Name == "FailedWritingMessage")) { var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => { diff --git a/src/Testing/src/Logging/BeginScopeContext.cs b/src/Testing/src/Logging/BeginScopeContext.cs new file mode 100644 index 0000000000..14ef991e0d --- /dev/null +++ b/src/Testing/src/Logging/BeginScopeContext.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class BeginScopeContext + { + public object Scope { get; set; } + + public string LoggerName { get; set; } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/ITestSink.cs b/src/Testing/src/Logging/ITestSink.cs new file mode 100644 index 0000000000..b328e5c595 --- /dev/null +++ b/src/Testing/src/Logging/ITestSink.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Logging.Testing +{ + public interface ITestSink + { + event Action MessageLogged; + + event Action ScopeStarted; + + Func WriteEnabled { get; set; } + + Func BeginEnabled { get; set; } + + IProducerConsumerCollection Scopes { get; set; } + + IProducerConsumerCollection Writes { get; set; } + + void Write(WriteContext context); + + void Begin(BeginScopeContext context); + } +} diff --git a/src/Testing/src/Logging/LogLevelAttribute.cs b/src/Testing/src/Logging/LogLevelAttribute.cs new file mode 100644 index 0000000000..74aa395d4b --- /dev/null +++ b/src/Testing/src/Logging/LogLevelAttribute.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)] + public class LogLevelAttribute : Attribute + { + public LogLevelAttribute(LogLevel logLevel) + { + LogLevel = logLevel; + } + + public LogLevel LogLevel { get; } + } +} diff --git a/src/Testing/src/Logging/LogValuesAssert.cs b/src/Testing/src/Logging/LogValuesAssert.cs new file mode 100644 index 0000000000..ef2ff1f406 --- /dev/null +++ b/src/Testing/src/Logging/LogValuesAssert.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public static class LogValuesAssert + { + /// + /// Asserts that the given key and value are present in the actual values. + /// + /// The key of the item to be found. + /// The value of the item to be found. + /// The actual values. + public static void Contains( + string key, + object value, + IEnumerable> actualValues) + { + Contains(new[] { new KeyValuePair(key, value) }, actualValues); + } + + /// + /// Asserts that all the expected values are present in the actual values by ignoring + /// the order of values. + /// + /// Expected subset of values + /// Actual set of values + public static void Contains( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + if (expectedValues == null) + { + throw new ArgumentNullException(nameof(expectedValues)); + } + + if (actualValues == null) + { + throw new ArgumentNullException(nameof(actualValues)); + } + + var comparer = new LogValueComparer(); + + foreach (var expectedPair in expectedValues) + { + if (!actualValues.Contains(expectedPair, comparer)) + { + throw new EqualException( + expected: GetString(expectedValues), + actual: GetString(actualValues)); + } + } + } + + private static string GetString(IEnumerable> logValues) + { + return string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]")); + } + + private class LogValueComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return string.Equals(x.Key, y.Key) && object.Equals(x.Value, y.Value); + } + + public int GetHashCode(KeyValuePair obj) + { + // We are never going to put this KeyValuePair in a hash table, + // so this is ok. + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Testing/src/Logging/TestLogger.cs b/src/Testing/src/Logging/TestLogger.cs new file mode 100644 index 0000000000..1f1b1d6aba --- /dev/null +++ b/src/Testing/src/Logging/TestLogger.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLogger : ILogger + { + private object _scope; + private readonly ITestSink _sink; + private readonly string _name; + private readonly Func _filter; + + public TestLogger(string name, ITestSink sink, bool enabled) + : this(name, sink, _ => enabled) + { + } + + public TestLogger(string name, ITestSink sink, Func filter) + { + _sink = sink; + _name = name; + _filter = filter; + } + + public string Name { get; set; } + + public IDisposable BeginScope(TState state) + { + _scope = state; + + _sink.Begin(new BeginScopeContext() + { + LoggerName = _name, + Scope = state, + }); + + return TestDisposable.Instance; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + _sink.Write(new WriteContext() + { + LogLevel = logLevel, + EventId = eventId, + State = state, + Exception = exception, + Formatter = (s, e) => formatter((TState)s, e), + LoggerName = _name, + Scope = _scope + }); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel != LogLevel.None && _filter(logLevel); + } + + private class TestDisposable : IDisposable + { + public static readonly TestDisposable Instance = new TestDisposable(); + + public void Dispose() + { + // intentionally does nothing + } + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/TestLoggerFactory.cs b/src/Testing/src/Logging/TestLoggerFactory.cs new file mode 100644 index 0000000000..a7f2f1398c --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerFactory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLoggerFactory : ILoggerFactory + { + private readonly ITestSink _sink; + private readonly bool _enabled; + + public TestLoggerFactory(ITestSink sink, bool enabled) + { + _sink = sink; + _enabled = enabled; + } + + public ILogger CreateLogger(string name) + { + return new TestLogger(name, _sink, _enabled); + } + + public void AddProvider(ILoggerProvider provider) + { + } + + public void Dispose() + { + } + } +} diff --git a/src/Testing/src/Logging/TestLoggerProvider.cs b/src/Testing/src/Logging/TestLoggerProvider.cs new file mode 100644 index 0000000000..e604bda36e --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLoggerProvider : ILoggerProvider + { + private readonly ITestSink _sink; + + public TestLoggerProvider(ITestSink sink) + { + _sink = sink; + } + + public ILogger CreateLogger(string categoryName) + { + return new TestLogger(categoryName, _sink, enabled: true); + } + + public void Dispose() + { + } + } +} diff --git a/src/Testing/src/Logging/TestLoggerT.cs b/src/Testing/src/Logging/TestLoggerT.cs new file mode 100644 index 0000000000..096bb96535 --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerT.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLogger : ILogger + { + private readonly ILogger _logger; + + public TestLogger(TestLoggerFactory factory) + { + _logger = factory.CreateLogger(); + } + + public IDisposable BeginScope(TState state) + { + return _logger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _logger.IsEnabled(logLevel); + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + _logger.Log(logLevel, eventId, state, exception, formatter); + } + } +} diff --git a/src/Testing/src/Logging/TestSink.cs b/src/Testing/src/Logging/TestSink.cs new file mode 100644 index 0000000000..5285b3068f --- /dev/null +++ b/src/Testing/src/Logging/TestSink.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestSink : ITestSink + { + private ConcurrentQueue _scopes; + private ConcurrentQueue _writes; + + public TestSink( + Func writeEnabled = null, + Func beginEnabled = null) + { + WriteEnabled = writeEnabled; + BeginEnabled = beginEnabled; + + _scopes = new ConcurrentQueue(); + _writes = new ConcurrentQueue(); + } + + public Func WriteEnabled { get; set; } + + public Func BeginEnabled { get; set; } + + public IProducerConsumerCollection Scopes { get => _scopes; set => _scopes = new ConcurrentQueue(value); } + + public IProducerConsumerCollection Writes { get => _writes; set => _writes = new ConcurrentQueue(value); } + + public event Action MessageLogged; + + public event Action ScopeStarted; + + public void Write(WriteContext context) + { + if (WriteEnabled == null || WriteEnabled(context)) + { + _writes.Enqueue(context); + } + MessageLogged?.Invoke(context); + } + + public void Begin(BeginScopeContext context) + { + if (BeginEnabled == null || BeginEnabled(context)) + { + _scopes.Enqueue(context); + } + ScopeStarted?.Invoke(context); + } + + public static bool EnableWithTypeName(WriteContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + + public static bool EnableWithTypeName(BeginScopeContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/WriteContext.cs b/src/Testing/src/Logging/WriteContext.cs new file mode 100644 index 0000000000..0ecfc8f1a9 --- /dev/null +++ b/src/Testing/src/Logging/WriteContext.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class WriteContext + { + public LogLevel LogLevel { get; set; } + + public EventId EventId { get; set; } + + public object State { get; set; } + + public Exception Exception { get; set; } + + public Func Formatter { get; set; } + + public object Scope { get; set; } + + public string LoggerName { get; set; } + + public string Message + { + get + { + return Formatter(State, Exception); + } + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs new file mode 100644 index 0000000000..7d053d45dd --- /dev/null +++ b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging +{ + public static class XunitLoggerFactoryExtensions + { + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output)); + return builder; + } + + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel)); + return builder; + } + + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel, logStart)); + return builder; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output)); + return loggerFactory; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel)); + return loggerFactory; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart)); + return loggerFactory; + } + } +} diff --git a/src/Testing/src/Logging/XunitLoggerProvider.cs b/src/Testing/src/Logging/XunitLoggerProvider.cs new file mode 100644 index 0000000000..3a1d751413 --- /dev/null +++ b/src/Testing/src/Logging/XunitLoggerProvider.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Text; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class XunitLoggerProvider : ILoggerProvider + { + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } + } + + public class XunitLogger : ILogger + { + private static readonly string[] NewLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3")}s" : DateTimeOffset.UtcNow.ToString("s"); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) + => new NullScope(); + + private class NullScope : IDisposable + { + public void Dispose() + { + } + } + } +} diff --git a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj index 5ddad7b645..7d95e026a7 100644 --- a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj +++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj @@ -19,7 +19,9 @@ - + + + diff --git a/src/Testing/test/LogValuesAssertTest.cs b/src/Testing/test/LogValuesAssertTest.cs new file mode 100644 index 0000000000..dc2db9d83d --- /dev/null +++ b/src/Testing/test/LogValuesAssertTest.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing.Tests +{ + public class LogValuesAssertTest + { + public static TheoryData< + IEnumerable>, + IEnumerable>> ExpectedValues_SubsetOf_ActualValuesData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new KeyValuePair[] { }, + new KeyValuePair[] { } + }, + { + // subset + new KeyValuePair[] { }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + } + }, + { + // subset + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something") + } + }, + { + // equal number of values + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + } + } + }; + } + } + + [Theory] + [MemberData(nameof(ExpectedValues_SubsetOf_ActualValuesData))] + public void Asserts_Success_ExpectedValues_SubsetOf_ActualValues( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + LogValuesAssert.Contains(expectedValues, actualValues); + } + + public static TheoryData< + IEnumerable>, + IEnumerable>> ExpectedValues_MoreThan_ActualValuesData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new KeyValuePair[] { } + }, + { + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + } + } + }; + } + } + + [Theory] + [MemberData(nameof(ExpectedValues_MoreThan_ActualValuesData))] + public void Asserts_Failure_ExpectedValues_MoreThan_ActualValues( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + var equalException = Assert.Throws( + () => LogValuesAssert.Contains(expectedValues, actualValues)); + + Assert.Equal(GetString(expectedValues), equalException.Expected); + Assert.Equal(GetString(actualValues), equalException.Actual); + } + + [Fact] + public void Asserts_Success_IgnoringOrderOfItems() + { + // Arrange + var expectedLogValues = new[] + { + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }; + var actualLogValues = new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteValue", "Failure"), + }; + + // Act && Assert + LogValuesAssert.Contains(expectedLogValues, actualLogValues); + } + + [Fact] + public void Asserts_Success_OnSpecifiedKeyAndValue() + { + // Arrange + var actualLogValues = new[] + { + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }; + + // Act && Assert + LogValuesAssert.Contains("RouteKey", "id", actualLogValues); + } + + public static TheoryData< + IEnumerable>, + IEnumerable>> CaseSensitivityComparisionData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }, + new[] + { + new KeyValuePair("ROUTEKEY", "id"), + new KeyValuePair("RouteValue", "Failure"), + } + }, + { + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }, + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "FAILURE"), + } + } + }; + } + } + + [Theory] + [MemberData(nameof(CaseSensitivityComparisionData))] + public void DefaultComparer_Performs_CaseSensitiveComparision( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + var equalException = Assert.Throws( + () => LogValuesAssert.Contains(expectedValues, actualValues)); + + Assert.Equal(GetString(expectedValues), equalException.Expected); + Assert.Equal(GetString(actualValues), equalException.Actual); + } + + private string GetString(IEnumerable> logValues) + { + return logValues == null ? + "Null" : + string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]")); + } + } +} diff --git a/src/Testing/test/XunitLoggerProviderTest.cs b/src/Testing/test/XunitLoggerProviderTest.cs new file mode 100644 index 0000000000..e43447c465 --- /dev/null +++ b/src/Testing/test/XunitLoggerProviderTest.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Logging.Testing.Tests +{ + public class XunitLoggerProviderTest + { + [Fact] + public void LoggerProviderWritesToTestOutputHelper() + { + var testTestOutputHelper = new TestTestOutputHelper(); + + var loggerFactory = CreateTestLogger(builder => builder + .SetMinimumLevel(LogLevel.Trace) + .AddXunit(testTestOutputHelper)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is some great information"); + logger.LogTrace("This is some unimportant information"); + + var expectedOutput = + "| [TIMESTAMP] TestCategory Information: This is some great information" + Environment.NewLine + + "| [TIMESTAMP] TestCategory Trace: This is some unimportant information" + Environment.NewLine; + + Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderDoesNotWriteLogMessagesBelowMinimumLevel() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + .AddXunit(testTestOutputHelper, LogLevel.Warning)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is some great information"); + logger.LogError("This is a bad error"); + + Assert.Equal("| [TIMESTAMP] TestCategory Error: This is a bad error" + Environment.NewLine, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderPrependsPrefixToEachLine() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + .AddXunit(testTestOutputHelper)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message"); + + // The lines after the first one are indented more because the indentation was calculated based on the timestamp's actual length. + var expectedOutput = + "| [TIMESTAMP] TestCategory Information: This is a" + Environment.NewLine + + "| multi-line" + Environment.NewLine + + "| message" + Environment.NewLine; + + Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderDoesNotThrowIfOutputHelperThrows() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + + .AddXunit(testTestOutputHelper)); + + testTestOutputHelper.Throw = true; + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message"); + + Assert.Equal(0, testTestOutputHelper.Output.Length); + } + + private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+"); + + private string MakeConsistent(string input) => TimestampRegex.Replace(input, "TIMESTAMP"); + + private static ILoggerFactory CreateTestLogger(Action configure) + { + return new ServiceCollection() + .AddLogging(configure) + .BuildServiceProvider() + .GetRequiredService(); + } + } +} diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 5651ba4622..6114e56a44 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests public ITestOutputHelper Output { get; } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() { try @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() { // Arrange @@ -154,8 +154,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [Fact] public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsIncorrect() { _fixture.CleanupCertificates(); @@ -170,8 +169,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Empty(httpsCertificateList); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [Fact] public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersionField() { _fixture.CleanupCertificates(); @@ -188,7 +186,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero() { _fixture.CleanupCertificates(); @@ -203,7 +201,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer() { _fixture.CleanupCertificates(); diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs index 1abb14ed7b..6e25f0ccd6 100644 --- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs @@ -446,34 +446,5 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); Assert.False(File.Exists(jsonFile)); } - - [Fact] - public void OpenApi_Add_URL_ActualResponse() - { - var project = CreateBasicProject(withOpenApi: false); - - var app = GetApplication(realHttp: true); - var url = ActualUrl; - var run = app.Execute(new[] { "add", "url", url }); - - AssertNoErrors(run); - - app = GetApplication(realHttp: true); - run = app.Execute(new[] { "add", "url", url }); - - AssertNoErrors(run); - - // csproj contents - var csproj = new FileInfo(project.Project.Path); - using var csprojStream = csproj.OpenRead(); - using var reader = new StreamReader(csprojStream); - var content = reader.ReadToEnd(); - var escapedPkgRef = Regex.Escape("", content); - } } } diff --git a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs index 5b8b038596..cbbcee8233 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs +++ b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs @@ -76,7 +76,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory); // Load the project file as XML - var projectDocument = XDocument.Load(projectPath); + var projectDocument = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); // Accept the `--id` CLI option to the main app string newSecretsId = string.IsNullOrWhiteSpace(OverrideId) @@ -120,19 +120,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal } // Add UserSecretsId element + propertyGroup.Add(" "); propertyGroup.Add(new XElement("UserSecretsId", newSecretsId)); + propertyGroup.Add($"{Environment.NewLine} "); } var settings = new XmlWriterSettings { - Indent = true, OmitXmlDeclaration = true, }; - using (var xw = XmlWriter.Create(projectPath, settings)) - { - projectDocument.Save(xw); - } + using var xw = XmlWriter.Create(projectPath, settings); + projectDocument.Save(xw); context.Reporter.Output(Resources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath)); } diff --git a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs index d299c208ca..acfda466b7 100644 --- a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs +++ b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Xml.Linq; using Microsoft.AspNetCore.Testing; @@ -72,6 +73,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests } [Fact] + [QuarantinedTest] public void DoesNotGenerateIdForProjectWithSecretId() { const string SecretId = "AlreadyExists"; @@ -97,6 +99,22 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests Assert.Null(projectDocument.Declaration); } + [Fact] + public void DoesNotRemoveBlankLines() + { + var projectDir = _fixture.CreateProject(null); + var projectFile = Path.Combine(projectDir, "TestProject.csproj"); + var projectDocumentWithoutSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace); + var lineCountWithoutSecret = projectDocumentWithoutSecret.ToString().Split(Environment.NewLine).Length; + + new InitCommand(null, null).Execute(MakeCommandContext(), projectDir); + + var projectDocumentWithSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace); + var lineCountWithSecret = projectDocumentWithSecret.ToString().Split(Environment.NewLine).Length; + + Assert.True(lineCountWithSecret == lineCountWithoutSecret + 1); + } + [Fact] public void OverridesIdForProjectWithSecretId() { diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs index 40c2af5214..478fc6c846 100644 --- a/src/Tools/dotnet-watch/test/ProgramTests.cs +++ b/src/Tools/dotnet-watch/test/ProgramTests.cs @@ -24,6 +24,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests } [Fact] + [QuarantinedTest] public async Task ConsoleCancelKey() { _tempDir