diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 00e3e87beec2..c31c8405a9b1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,6 +16,11 @@ Testing: - Examples: - `dotnet test test/dotnet.Tests/dotnet.Tests.csproj --filter "Name~ItShowsTheAppropriateMessageToTheUser"` - `dotnet exec artifacts/bin/redist/Debug/dotnet.Tests.dll -method "*ItShowsTheAppropriateMessageToTheUser*"` +- To test CLI command changes: + - Build the redist SDK: `./build.sh` from repo root + - Create a dogfood environment: `source eng/dogfood.sh` + - Test commands in the dogfood shell (e.g., `dnx --help`, `dotnet tool install --help`) + - The dogfood script sets up PATH and environment to use the newly built SDK Output Considerations: - When considering how output should look, solicit advice from baronfel. diff --git a/.vsts-ci.yml b/.vsts-ci.yml index cdee394e48f5..82dded316160 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -102,6 +102,7 @@ extends: oneESCompat: templateFolderName: templates-official publishTaskPrefix: 1ES. + populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) locBranch: release/10.0.1xx # WORKAROUND: BinSkim requires the folder exist prior to scanning. @@ -141,6 +142,14 @@ extends: _SignType: real dependsOn: Official_windows_x64 downloadManifestMsiPackages: true + ### TestTemplatesCG ### + # Note: This job is only used to allow the test templates to be built locally on the agent as opposed to Helix. + # The tests acquire the templates' PackageReferences from NuGet, which allows them to be scanned by CG (component governance). + # CG is only ran internally, so this job makes sense to only run alongside of the official jobs. + - categoryName: TestTemplatesCG + testProjects: $(Build.SourcesDirectory)/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj + testRunnerAdditionalArguments: -class Microsoft.DotNet.Cli.New.IntegrationTests.DotnetNewTestTemplatesTests + publishXunitResults: true ############### LINUX ############### - template: /eng/pipelines/templates/jobs/sdk-job-matrix.yml@self @@ -153,6 +162,7 @@ extends: oneESCompat: templateFolderName: templates-official publishTaskPrefix: 1ES. + populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) ${{ if and(eq(parameters.runTestBuild, false), ne(variables['Build.Reason'], 'PullRequest')) }}: timeoutInMinutes: 90 @@ -235,6 +245,7 @@ extends: oneESCompat: templateFolderName: templates-official publishTaskPrefix: 1ES. + populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) ${{ if and(eq(parameters.runTestBuild, false), ne(variables['Build.Reason'], 'PullRequest')) }}: timeoutInMinutes: 90 @@ -260,6 +271,8 @@ extends: vmImage: macOS-latest os: macOS helixTargetQueue: osx.13.arm64 + populateInternalRuntimeVariables: true + runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) macOSJobParameterSets: - categoryName: TestBuild targetArchitecture: arm64 @@ -270,7 +283,9 @@ extends: - template: /eng/dotnet-format/dotnet-format-integration.yml@self parameters: oneESCompat: + templateFolderName: templates-official publishTaskPrefix: 1ES. + populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) ############### PUBLISH STAGE ############### diff --git a/Directory.Build.targets b/Directory.Build.targets index f8d73e5ff744..464f65cfee17 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -73,7 +73,7 @@ $(MicrosoftAspNetCoreAppRefPackageVersion) - ${SupportedRuntimeIdentifiers} + $(SupportedRuntimeIdentifiers) $(MicrosoftAspNetCoreAppRefPackageVersion) $(MicrosoftAspNetCoreAppRefPackageVersion) diff --git a/Directory.Packages.props b/Directory.Packages.props index 928b44f65d5e..ecc47ba3f164 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,11 +5,11 @@ $(NoWarn);NU1507 - + - + @@ -94,7 +94,7 @@ - + diff --git a/NuGet.config b/NuGet.config index 09870da28882..4303832450f7 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,6 +3,8 @@ + + diff --git a/build/RunTestTemplateTests.ps1 b/build/RunTestTemplateTests.ps1 deleted file mode 100644 index 5fe903b77f34..000000000000 --- a/build/RunTestTemplateTests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -<# -.SYNOPSIS - Runs Microsoft.TestTemplates.Acceptance.Tests.dll in the dogfood environment. -.DESCRIPTION - This script enters the dogfood environment and runs the RunTestTemplateTests tests. -#> -[CmdletBinding(PositionalBinding=$false)] -Param( - [string] $configuration = "Release" -) - -function Run-TestTemplateTests { - $ErrorActionPreference = 'Stop' - $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') - $classNameFilter = "--filter" - $filterValue = "FullyQualifiedName~Microsoft.DotNet.Cli.New.IntegrationTests.DotnetNewTestTemplatesTests" - $TestDll = Join-Path $RepoRoot "artifacts\bin\dotnet-new.IntegrationTests\$configuration\dotnet-new.IntegrationTests.dll" - - # Check if the test DLL exists - if (-not (Test-Path $TestDll)) { - Write-Error "Test DLL not found at: $TestDll" - return 1 - } - - Write-Host "Running tests for test templates in the dogfood environment..." -ForegroundColor Cyan - - # Call dogfood.ps1 directly instead of through dogfood.cmd to avoid the -NoExit parameter - $dogfoodPs1 = Join-Path $RepoRoot "eng\dogfood.ps1" - - Write-Host "Executing: dotnet test $TestDll via dogfood environment" -ForegroundColor Gray - # Pass the command directly to the dogfood.ps1 script - & $dogfoodPs1 -configuration $configuration -command @("dotnet", "test", $TestDll, $classNameFilter, $filterValue) - - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-Error "Tests failed with exit code: $exitCode" - } else { - Write-Host "Tests completed successfully!" -ForegroundColor Green - } - - return $exitCode -} - -# Execute the function using Invoke-Command -$exitCode = Invoke-Command -ScriptBlock ${function:Run-TestTemplateTests} -exit $exitCode diff --git a/documentation/general/analyzer-redirecting.md b/documentation/general/analyzer-redirecting.md index 208c28734787..e999b98d76fe 100644 --- a/documentation/general/analyzer-redirecting.md +++ b/documentation/general/analyzer-redirecting.md @@ -28,21 +28,36 @@ Targeting an SDK (and hence also loading analyzers) with newer major version in - Note that when `IAnalyzerAssemblyRedirector` is involved, Roslyn is free to not use shadow copy loading and instead load the DLLs directly. +- It is possible to opt out of analyzer redirecting by setting environment variable `DOTNET_ANALYZER_REDIRECTING=0`. + That is an unsupported scenario though and compiler version mismatch errors will likely occur. + ## Details The VSIX contains some analyzers, for example: ``` -AspNetCoreAnalyzers\9.0.0-preview.5.24306.11\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll -NetCoreAnalyzers\9.0.0-preview.5.24306.7\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll -WindowsDesktopAnalyzers\9.0.0-preview.5.24306.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll -SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll -WebSDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll +AspNetCoreAnalyzers\analyzers\dotnet\cs\Microsoft.AspNetCore.App.Analyzers.dll +NetCoreAnalyzers\analyzers\dotnet\cs\System.Text.RegularExpressions.Generator.dll +WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll +SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll +WebSDKAnalyzers\Sdks\Microsoft.NET.Sdk.Web\analyzers\cs\Microsoft.AspNetCore.Analyzers.dll +``` + +And metadata at `metadata.json`: + +```json +{ + "AspNetCoreAnalyzers": "9.0.0-preview.5.24306.11", + "NetCoreAnalyzers": "9.0.0-preview.5.24306.7", + "WindowsDesktopAnalyzers": "9.0.0-preview.5.24306.8", + "SDKAnalyzers": "9.0.100-dev", + "WebSDKAnalyzers": "9.0.100-dev", +} ``` Given an analyzer assembly load going through our `IAnalyzerAssemblyRedirector`, we will redirect it if the original path of the assembly being loaded matches the path of a VSIX-deployed analyzer - -only segments of these paths starting after the version segment are compared, +only relevant segments (see example below) of these paths are compared, plus the major and minor component of the versions must match. For example, the analyzer @@ -54,10 +69,10 @@ C:\Program Files\dotnet\sdk\9.0.100-preview.5.24307.3\Sdks\Microsoft.NET.Sdk\ana will be redirected to ``` -{VSIX}\SDKAnalyzers\9.0.100-dev\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll +{VSIX}\SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll ``` -because +where `metadata.json` has `"SDKAnalyzers": "9.0.100-dev"`, because 1. the suffix `Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll` matches, and 2. the version `9.0.100-preview.5.24307.3` has the same major and minor component (`9.0`) as the version `9.0.100-dev` (both versions are read from the paths, not DLL metadata). @@ -65,4 +80,9 @@ because Analyzers that cannot be matched will continue to be loaded from the SDK (and will fail to load if they reference Roslyn that is newer than is in VS). +### Implementation + +Analyzer DLLs are contained in transport package `VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers`. +The redirecting logic lives in "system" VS extension `Microsoft.Net.Sdk.AnalyzerRedirecting`. + [torn-sdk]: https://github.com/dotnet/sdk/issues/42087 diff --git a/documentation/manpages/sdk/dotnet-build.1 b/documentation/manpages/sdk/dotnet-build.1 index 99fe1e657990..fb7ce5950153 100644 --- a/documentation/manpages/sdk/dotnet-build.1 +++ b/documentation/manpages/sdk/dotnet-build.1 @@ -14,19 +14,19 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-build" "1" "2025-06-13" "" ".NET Documentation" +.TH "dotnet-build" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet build .PP -\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET Core 3.1 SDK and later versions +\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET 6 and later versions .SH NAME .PP -dotnet-build - Builds a project and all of its dependencies. +dotnet-build - Builds a project, solution, or file-based app and all of its dependencies. .SH SYNOPSIS .IP .nf \f[C] -dotnet build [|] [-a|--arch ] +dotnet build [||] [-a|--arch ] [--artifacts-path ] [-c|--configuration ] [-f|--framework ] [--disable-build-servers] @@ -35,7 +35,7 @@ dotnet build [|] [-a|--arch ] [-o|--output ] [-p|--property:=] [-r|--runtime ] - [--self-contained [true|false]] [--source ] + [-sc|--self-contained [true|false]] [--source ] [--tl:[auto|on|off]] [--use-current-runtime, --ucr [true|false]] [-v|--verbosity ] [--version-suffix ] @@ -44,11 +44,11 @@ dotnet build -h|--help .fi .SH DESCRIPTION .PP -The \f[V]dotnet build\f[R] command builds the project and its dependencies into a set of binaries. +The \f[V]dotnet build\f[R] command builds the project, solution, or file-based app and its dependencies into a set of binaries. The binaries include the project\[cq]s code in Intermediate Language (IL) files with a \f[I].dll\f[R] extension. Depending on the project type and settings, other files may be included, such as: .IP \[bu] 2 -An executable that can be used to run the application, if the project type is an executable targeting .NET Core 3.0 or later. +An executable that can be used to run the application. .IP \[bu] 2 Symbol files used for debugging with a \f[I].pdb\f[R] extension. .IP \[bu] 2 @@ -58,12 +58,6 @@ A \f[I].runtimeconfig.json\f[R] file, which specifies the shared runtime and its .IP \[bu] 2 Other libraries that the project depends on (via project references or NuGet package references). .PP -For executable projects targeting versions earlier than .NET Core 3.0, library dependencies from NuGet are typically NOT copied to the output folder. -They\[cq]re resolved from the NuGet global packages folder at run time. -With that in mind, the product of \f[V]dotnet build\f[R] isn\[cq]t ready to be transferred to another machine to run. -To create a version of the application that can be deployed, you need to publish it (for example, with the dotnet publish command). -For more information, see .NET Application Deployment. -.PP For executable projects targeting .NET Core 3.0 and later, library dependencies are copied to the output folder. This means that if there isn\[cq]t any other publish-specific logic (such as Web projects have), the build output should be deployable. .SS Implicit restore @@ -98,7 +92,8 @@ To produce a library, omit the \f[V]\f[R] property or change its val The IL DLL for a library doesn\[cq]t contain entry points and can\[cq]t be executed. .SS MSBuild .PP -\f[V]dotnet build\f[R] uses MSBuild to build the project, so it supports both parallel and incremental builds. +\f[V]dotnet build\f[R] uses MSBuild to build the project, solution, or file-based app. +It supports both parallel and incremental builds. For more information, see Incremental Builds. .PP In addition to its options, the \f[V]dotnet build\f[R] command accepts MSBuild options, such as \f[V]-p\f[R] for setting properties or \f[V]-l\f[R] to define a logger. @@ -117,10 +112,19 @@ If the download is still running when this command finishes, the download is sto For more information, see Advertising manifests. .SH ARGUMENTS .PP -\f[V]PROJECT | SOLUTION\f[R] +\f[V]PROJECT | SOLUTION | FILE\f[R] .PP -The project or solution file to build. -If a project or solution file isn\[cq]t specified, MSBuild searches the current working directory for a file that has a file extension that ends in either \f[I]proj\f[R] or \f[I]sln\f[R] and uses that file. +The project or solution or C# (file-based app) file to operate on. +If a file isn\[cq]t specified, MSBuild searches the current directory for a project or solution. +.IP \[bu] 2 +\f[V]PROJECT\f[R] is the path and filename of a C#, F#, or Visual Basic project file, or the path to a directory that contains a C#, F#, or Visual Basic project file. +.IP \[bu] 2 +\f[V]SOLUTION\f[R] is the path and filename of a solution file (\f[I].sln\f[R] or \f[I].slnx\f[R] extension), or the path to a directory that contains a solution file. +.IP \[bu] 2 +\f[V]FILE\f[R] is an argument added in .NET 10. +The path and filename of a file-based app. +File-based apps are contained within a single file that is built and run without a corresponding project (\f[I].csproj\f[R]) file. +For more information, see Build file-based C# apps. .SH OPTIONS .IP \[bu] 2 \f[B]\f[VB]-a|--arch \f[B]\f[R] @@ -285,13 +289,13 @@ The URI of the NuGet package source to use during the restore operation. \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. @@ -348,6 +352,18 @@ dotnet build .fi .RE .IP \[bu] 2 +Build a file-based app: +.RS 2 +.IP +.nf +\f[C] +dotnet build MyProject.cs +\f[R] +.fi +.PP +File-based app support was added in .NET SDK 10.0.100. +.RE +.IP \[bu] 2 Build a project and its dependencies using Release configuration: .RS 2 .IP diff --git a/documentation/manpages/sdk/dotnet-clean.1 b/documentation/manpages/sdk/dotnet-clean.1 index a259f0fe2df5..d59683069976 100644 --- a/documentation/manpages/sdk/dotnet-clean.1 +++ b/documentation/manpages/sdk/dotnet-clean.1 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-clean" "1" "2025-06-13" "" ".NET Documentation" +.TH "dotnet-clean" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet clean .PP -\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET Core 3.1 SDK and later versions +\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET 6 and later versions .SH NAME .PP dotnet-clean - Cleans the output of a project. @@ -26,7 +26,7 @@ dotnet-clean - Cleans the output of a project. .IP .nf \f[C] -dotnet clean [|] [--artifacts-path ] +dotnet clean [||] [--artifacts-path ] [-c|--configuration ] [-f|--framework ] [--interactive] [--nologo] [-o|--output ] @@ -44,10 +44,19 @@ Only the outputs created during the build are cleaned. Both intermediate (\f[I]obj\f[R]) and final output (\f[I]bin\f[R]) folders are cleaned. .SH ARGUMENTS .PP -\f[V]PROJECT | SOLUTION\f[R] +\f[V]PROJECT | SOLUTION | FILE\f[R] .PP -The MSBuild project or solution to clean. -If a project or solution file is not specified, MSBuild searches the current working directory for a file that has a file extension that ends in \f[I]proj\f[R] or \f[I]sln\f[R], and uses that file. +The project or solution or C# (file-based app) file to operate on. +If a file isn\[cq]t specified, MSBuild searches the current directory for a project or solution. +.IP \[bu] 2 +\f[V]PROJECT\f[R] is the path and filename of a C#, F#, or Visual Basic project file, or the path to a directory that contains a C#, F#, or Visual Basic project file. +.IP \[bu] 2 +\f[V]SOLUTION\f[R] is the path and filename of a solution file (\f[I].sln\f[R] or \f[I].slnx\f[R] extension), or the path to a directory that contains a solution file. +.IP \[bu] 2 +\f[V]FILE\f[R] is an argument added in .NET 10. +The path and filename of a file-based app. +File-based apps are contained within a single file that is built and run without a corresponding project (\f[I].csproj\f[R]) file. +For more information, see Build file-based C# apps. .SH OPTIONS .IP \[bu] 2 \f[B]\f[VB]--artifacts-path \f[B]\f[R] @@ -119,13 +128,13 @@ This is used when a self-contained deployment was created. \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. @@ -164,6 +173,18 @@ dotnet clean .fi .RE .IP \[bu] 2 +Clean a file-based program: +.RS 2 +.IP +.nf +\f[C] +dotnet clean Program.cs. +\f[R] +.fi +.PP +File-based app support was added in .NET SDK 10.0.100. +.RE +.IP \[bu] 2 Clean a project built using the Release configuration: .RS 2 .IP diff --git a/documentation/manpages/sdk/dotnet-pack.1 b/documentation/manpages/sdk/dotnet-pack.1 index 729fa6810b4c..58e02e3e5683 100644 --- a/documentation/manpages/sdk/dotnet-pack.1 +++ b/documentation/manpages/sdk/dotnet-pack.1 @@ -15,7 +15,7 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-pack" "1" "2025-06-13" "" ".NET Documentation" +.TH "dotnet-pack" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet pack .PP @@ -202,13 +202,13 @@ For more information, see .NET Blog: .NET Framework 4.5.1 Supports Microsoft Sec \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. diff --git a/documentation/manpages/sdk/dotnet-publish.1 b/documentation/manpages/sdk/dotnet-publish.1 index 64610ed45272..77da5ae6f4be 100644 --- a/documentation/manpages/sdk/dotnet-publish.1 +++ b/documentation/manpages/sdk/dotnet-publish.1 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-publish" "1" "2025-08-29" "" ".NET Documentation" +.TH "dotnet-publish" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet publish .PP -\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET Core 3.1 SDK and later versions +\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET 6 and later versions .SH NAME .PP dotnet-publish - Publishes the application and its dependencies to a folder for deployment to a hosting system. @@ -26,7 +26,7 @@ dotnet-publish - Publishes the application and its dependencies to a folder for .IP .nf \f[C] -dotnet publish [|] [-a|--arch ] +dotnet publish [||] [-a|--arch ] [--artifacts-path ] [-c|--configuration ] [--disable-build-servers] [-f|--framework ] [--force] [--interactive] @@ -172,18 +172,20 @@ When you run this command, it initiates an asynchronous background download of a If the download is still running when this command finishes, the download is stopped. For more information, see Advertising manifests. .SH ARGUMENTS -.IP \[bu] 2 -\f[B]\f[VB]PROJECT|SOLUTION\f[B]\f[R] -.RS 2 .PP -The project or solution to publish. +\f[V]PROJECT | SOLUTION | FILE\f[R] +.PP +The project or solution or C# (file-based app) file to operate on. +If a file isn\[cq]t specified, MSBuild searches the current directory for a project or solution. .IP \[bu] 2 \f[V]PROJECT\f[R] is the path and filename of a C#, F#, or Visual Basic project file, or the path to a directory that contains a C#, F#, or Visual Basic project file. -If the directory is not specified, it defaults to the current directory. .IP \[bu] 2 \f[V]SOLUTION\f[R] is the path and filename of a solution file (\f[I].sln\f[R] or \f[I].slnx\f[R] extension), or the path to a directory that contains a solution file. -If the directory is not specified, it defaults to the current directory. -.RE +.IP \[bu] 2 +\f[V]FILE\f[R] is an argument added in .NET 10. +The path and filename of a file-based app. +File-based apps are contained within a single file that is built and run without a corresponding project (\f[I].csproj\f[R]) file. +For more information, see Build file-based C# apps. .SH OPTIONS .IP \[bu] 2 \f[B]\f[VB]-a|--arch \f[B]\f[R] @@ -320,15 +322,6 @@ If you specify a relative path when publishing a solution, all output for all pr To make publish output go to separate folders for each project, specify a relative path by using the msbuild \f[V]PublishDir\f[R] property instead of the \f[V]--output\f[R] option. For example, \f[V]dotnet publish -p:PublishDir=.\[rs]publish\f[R] sends publish output for each project to a \f[V]publish\f[R] folder under the folder that contains the project file. .RE -.IP \[bu] 2 -\&.NET Core 2.x SDK -.RS 2 -.PP -If you specify a relative path when publishing a project, the generated output directory is relative to the project file location, not to the current working directory. -.PP -If you specify a relative path when publishing a solution, each project\[cq]s output goes into a separate folder relative to the project file location. -If you specify an absolute path when publishing a solution, all publish output for all projects goes into the specified folder. -.RE .RE .IP \[bu] 2 \f[B]\f[VB]--os \f[B]\f[R] @@ -376,13 +369,13 @@ If you use this option, use \f[V]--self-contained\f[R] or \f[V]--no-self-contain \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. @@ -491,6 +484,18 @@ dotnet publish --no-dependencies \f[R] .fi .RE +.IP \[bu] 2 +Publish the file-based C# program \f[I]app.cs\f[R] in the current directory: +.RS 2 +.IP +.nf +\f[C] +dotnet publish app.cs +\f[R] +.fi +.PP +File-based program support was added in .NET SDK 10.0.100. +.RE .SH SEE ALSO .IP \[bu] 2 \&.NET application publishing overview diff --git a/documentation/manpages/sdk/dotnet-restore.1 b/documentation/manpages/sdk/dotnet-restore.1 index 498f0b210e84..334fba4e4b17 100644 --- a/documentation/manpages/sdk/dotnet-restore.1 +++ b/documentation/manpages/sdk/dotnet-restore.1 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-restore" "1" "2025-06-30" "" ".NET Documentation" +.TH "dotnet-restore" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet restore .PP -\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET Core 3.1 SDK and later versions +\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET 6 and later versions .SH NAME .PP dotnet-restore - Restores the dependencies and tools of a project. @@ -26,7 +26,7 @@ dotnet-restore - Restores the dependencies and tools of a project. .IP .nf \f[C] -dotnet restore [] [--configfile ] [--disable-build-servers] +dotnet restore [||] [--configfile ] [--disable-build-servers] [--disable-parallel] [-f|--force] [--force-evaluate] [--ignore-failed-sources] [--interactive] [--lock-file-path ] [--locked-mode] @@ -126,12 +126,20 @@ When you run this command, it initiates an asynchronous background download of a If the download is still running when this command finishes, the download is stopped. For more information, see Advertising manifests. .SH ARGUMENTS -.IP \[bu] 2 -\f[B]\f[VB]ROOT\f[B]\f[R] -.RS 2 .PP -Optional path to the project file to restore. -.RE +\f[V]PROJECT | SOLUTION | FILE\f[R] +.PP +The project or solution or C# (file-based app) file to operate on. +If a file isn\[cq]t specified, MSBuild searches the current directory for a project or solution. +.IP \[bu] 2 +\f[V]PROJECT\f[R] is the path and filename of a C#, F#, or Visual Basic project file, or the path to a directory that contains a C#, F#, or Visual Basic project file. +.IP \[bu] 2 +\f[V]SOLUTION\f[R] is the path and filename of a solution file (\f[I].sln\f[R] or \f[I].slnx\f[R] extension), or the path to a directory that contains a solution file. +.IP \[bu] 2 +\f[V]FILE\f[R] is an argument added in .NET 10. +The path and filename of a file-based app. +File-based apps are contained within a single file that is built and run without a corresponding project (\f[I].csproj\f[R]) file. +For more information, see Build file-based C# apps. .SH OPTIONS .IP \[bu] 2 \f[B]\f[VB]-a|--arch \f[B]\f[R] @@ -250,13 +258,13 @@ Multiple sources can be provided by specifying this option multiple times. \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. diff --git a/documentation/manpages/sdk/dotnet-run.1 b/documentation/manpages/sdk/dotnet-run.1 index 65cf2e8d10a8..c37a294d9f89 100644 --- a/documentation/manpages/sdk/dotnet-run.1 +++ b/documentation/manpages/sdk/dotnet-run.1 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-run" "1" "2025-06-13" "" ".NET Documentation" +.TH "dotnet-run" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet run .PP -\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET Core 3.1 SDK and later versions +\f[B]This article applies to:\f[R] \[u2714]\[uFE0F] .NET 6 and later versions .SH NAME .PP dotnet-run - Runs source code without any explicit compile or launch commands. @@ -26,8 +26,8 @@ dotnet-run - Runs source code without any explicit compile or launch commands. .IP .nf \f[C] -dotnet run [-a|--arch ] [-c|--configuration ] - [-e|--environment ] +dotnet run [] [-a|--arch ] [-c|--configuration ] + [-e|--environment ] [--file ] [-f|--framework ] [--force] [--interactive] [--launch-profile ] [--no-build] [--no-dependencies] [--no-launch-profile] [--no-restore] @@ -87,6 +87,14 @@ Short form options, such as \f[V]-s\f[R], are not supported. When you run this command, it initiates an asynchronous background download of advertising manifests for workloads. If the download is still running when this command finishes, the download is stopped. For more information, see Advertising manifests. +.SH ARGUMENTS +.PP +\f[V]\f[R] +.PP +Arguments passed to the application that is being run. +.PP +Any arguments that aren\[cq]t recognized by \f[V]dotnet run\f[R] are passed to the application. +To separate arguments for \f[V]dotnet run\f[R] from arguments for the application, use the \f[V]--\f[R] option. .SH OPTIONS .IP \[bu] 2 \f[B]\f[VB]--\f[B]\f[R] @@ -132,6 +140,34 @@ Builds and runs the app using the specified framework. The framework must be specified in the project file. .RE .IP \[bu] 2 +\f[B]\f[VB]--file \f[B]\f[R] +.RS 2 +.PP +The path to the file-based app to run. +If a path isn\[cq]t specified, the current directory is used to find and run the file. +For more information on file-based apps, see Build file-based C# apps. +.PP +On Unix, you can run file-based apps directly, using the source file name on the command line instead of \f[V]dotnet run\f[R]. +First, ensure the file has execute permissions. +Then, add a shebang line \f[V]#!\f[R] as the first line of the file, for example: +.IP +.nf +\f[C] +#!/usr/bin/env dotnet run +\f[R] +.fi +.PP +Then you can run the file directly from the command line: +.IP +.nf +\f[C] +\&./ConsoleApp.cs +\f[R] +.fi +.PP +Introduced in .NET SDK 10.0.100. +.RE +.IP \[bu] 2 \f[B]\f[VB]--force\f[B]\f[R] .RS 2 .PP @@ -245,13 +281,13 @@ For a list of Runtime Identifiers (RIDs), see the RID catalog. \f[B]\f[VB]--tl:[auto|on|off]\f[B]\f[R] .RS 2 .PP -Specifies whether the \f[I]terminal logger\f[R] should be used for the build output. +Specifies whether \f[I]Terminal Logger\f[R] should be used for the build output. The default is \f[V]auto\f[R], which first verifies the environment before enabling terminal logging. The environment check verifies that the terminal is capable of using modern output features and isn\[cq]t using a redirected standard output before enabling the new logger. \f[V]on\f[R] skips the environment check and enables terminal logging. \f[V]off\f[R] skips the environment check and uses the default console logger. .PP -The terminal logger shows you the restore phase followed by the build phase. +Terminal Logger shows you the restore phase followed by the build phase. During each phase, the currently building projects appear at the bottom of the terminal. Each project that\[cq]s building outputs both the MSBuild target currently being built and the amount of time spent on that target. You can search this information to learn more about the build. @@ -306,6 +342,18 @@ dotnet run .fi .RE .IP \[bu] 2 +Run the specified file-based app in the current directory: +.RS 2 +.IP +.nf +\f[C] +dotnet run --file ConsoleApp.cs +\f[R] +.fi +.PP +File-based app support was added in .NET SDK 10.0.100. +.RE +.IP \[bu] 2 Run the specified project: .RS 2 .IP @@ -345,3 +393,22 @@ dotnet run --verbosity m \f[R] .fi .RE +.IP \[bu] 2 +Run the project in the current directory using the specified framework and pass arguments to the application: +.RS 2 +.IP +.nf +\f[C] +dotnet run -f net6.0 -- arg1 arg2 +\f[R] +.fi +.PP +In the following example, three arguments are passed to the application. +One argument is passed using \f[V]-\f[R], and two arguments are passed after \f[V]--\f[R]: +.IP +.nf +\f[C] +dotnet run -f net6.0 -arg1 -- arg2 arg3 +\f[R] +.fi +.RE diff --git a/documentation/manpages/sdk/dotnet-watch.1 b/documentation/manpages/sdk/dotnet-watch.1 index 6a02e55593a9..538a5acd0632 100644 --- a/documentation/manpages/sdk/dotnet-watch.1 +++ b/documentation/manpages/sdk/dotnet-watch.1 @@ -14,7 +14,7 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "dotnet-watch" "1" "2025-06-13" "" ".NET Documentation" +.TH "dotnet-watch" "1" "2025-09-30" "" ".NET Documentation" .hy .SH dotnet watch .PP @@ -177,7 +177,7 @@ The class uses \f \f[B]\f[VB]DOTNET_WATCH_AUTO_RELOAD_WS_HOSTNAME\f[B]\f[R] .RS 2 .PP -As part of \f[V]dotnet watch\f[R], the browser refresh server mechanism reads this value to determine the WebSocket host environment's hostname. +As part of \f[V]dotnet watch\f[R], the browser refresh server mechanism reads this value to determine the WebSocket host environment. The value \f[V]127.0.0.1\f[R] is replaced by \f[V]localhost\f[R], and the \f[V]http://\f[R] and \f[V]https://\f[R] schemes are replaced with \f[V]ws://\f[R] and \f[V]wss://\f[R] respectively. .RE .IP \[bu] 2 diff --git a/documentation/project-docs/telemetry.md b/documentation/project-docs/telemetry.md index e7ec6a03e46a..e559f60bb25b 100644 --- a/documentation/project-docs/telemetry.md +++ b/documentation/project-docs/telemetry.md @@ -61,7 +61,7 @@ Every telemetry event automatically includes these common properties: | **Telemetry Profile** | Custom telemetry profile (if set via env var) | Custom value or null | | **Docker Container** | Whether running in Docker container | `True` or `False` | | **CI** | Whether running in CI environment | `True` or `False` | -| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude` or `cursor` | +| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude`, `cursor`, `gemini`, `copilot`, `generic_agent` | | **Current Path Hash** | SHA256 hash of current directory path | Hashed value | | **Machine ID** | SHA256 hash of machine MAC address (or GUID if unavailable) | Hashed value | | **Machine ID Old** | Legacy machine ID for compatibility | Hashed value | diff --git a/eng/Signing.props b/eng/Signing.props index 484697efecbc..872602d12f5c 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -84,6 +84,9 @@ + + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 749ba1b09122..4189c1aaab81 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,142 +6,142 @@ This file should be imported by eng/Versions.props - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-preview.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 17.15.0-preview-25460-104 - 17.15.0-preview-25460-104 - 7.0.0-preview.1.46104 - 10.0.0-beta.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 10.0.0-preview.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 2.0.0-preview.1.25460.104 - 2.2.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 14.0.100-rc2.25460.104 - 10.0.0-rc.2.25460.104 - 5.0.0-2.25460.104 - 5.0.0-2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-preview.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 18.0.0-preview-25509-106 + 18.0.0-preview-25509-106 + 7.0.0-rc.1006 + 10.0.0-beta.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 10.0.0-preview.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 2.0.0-preview.1.25509.106 + 2.2.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 14.0.100-rc2.25509.106 + 10.0.0-rtm.25509.106 + 5.0.0-2.25509.106 + 5.0.0-2.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 10.0.0-preview.7.25377.103 - 10.0.0-preview.25460.104 - 10.0.0-rc.2.25460.104 - 18.0.0-preview-25460-104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.0-beta.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 10.0.100-rc.2.25460.104 - 18.0.0-preview-25460-104 - 18.0.0-preview-25460-104 - 3.2.0-preview.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 7.0.0-preview.1.46104 - 10.0.0-rc.2.25460.104 - 2.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 - 10.0.0-rc.2.25460.104 + 10.0.0-preview.25509.106 + 10.0.0-rtm.25509.106 + 18.0.0-release-25509-106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.0-beta.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 10.0.100-rtm.25509.106 + 18.0.0-release-25509-106 + 18.0.0-release-25509-106 + 3.2.0-preview.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 7.0.0-rc.1006 + 10.0.0-rtm.25509.106 + 2.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 + 10.0.0-rtm.25509.106 2.1.0 - 1.9.0-preview.25470.1 - 3.11.0-preview.25470.1 + 2.1.0-preview.25508.5 + 4.1.0-preview.25508.5 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7b0fbbc2dcf4..d6edacbe4851 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,62 +1,62 @@ - + - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac @@ -68,170 +68,170 @@ https://github.com/dotnet/dotnet 6a953e76162f3f079405f80e28664fa51b136740 - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac - + https://github.com/microsoft/testfx - 7812bf1fe7289e9f528ced8a230502c1e0bfe50f + 9866a77221b818a70721106b9622eac95f81adec - + https://github.com/microsoft/testfx - 7812bf1fe7289e9f528ced8a230502c1e0bfe50f + 9866a77221b818a70721106b9622eac95f81adec - + https://github.com/dotnet/dotnet - eac14590f69f6876d418cef9e8fdd3f44f6ef0b2 + f448387a0e80f2fdeaec2d2f99ace7284fe37aac diff --git a/eng/Versions.props b/eng/Versions.props index 93158d233f90..d60cd6039953 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -38,8 +38,8 @@ 36 20 - 18 - 7 + 19 + 8 <_NET70ILLinkPackVersion>7.0.100-1.23211.1 diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index 792b60b49d42..9445c3143258 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -157,7 +157,7 @@ if ($dotnet31Source -ne $null) { AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -pwd $Password } -$dotnetVersions = @('5','6','7','8','9') +$dotnetVersions = @('5','6','7','8','9','10') foreach ($dotnetVersion in $dotnetVersions) { $feedPrefix = "dotnet" + $dotnetVersion; diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index facb415ca6ff..ddf4efc81a4a 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -99,7 +99,7 @@ if [ "$?" == "0" ]; then PackageSources+=('dotnet3.1-internal-transport') fi -DotNetVersions=('5' '6' '7' '8' '9') +DotNetVersions=('5' '6' '7' '8' '9' '10') for DotNetVersion in ${DotNetVersions[@]} ; do FeedPrefix="dotnet${DotNetVersion}"; diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index aba50e341034..37dff559fc1b 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -131,7 +131,7 @@ jobs: /p:ManifestsPath='$(Build.StagingDirectory)/AssetManifests' /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} /p:MaestroApiEndpoint=https://maestro.dot.net - /p:OfficialBuildId=$(Build.BuildNumber) + /p:OfficialBuildId=$(OfficialBuildId) condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index 947f0971eb5c..d805d5faeb94 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -34,9 +34,6 @@ parameters: # container and pool. platform: {} - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: '' # If set to true and running on a non-public project, @@ -97,4 +94,3 @@ jobs: parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} platform: ${{ parameters.platform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} diff --git a/eng/common/core-templates/jobs/source-build.yml b/eng/common/core-templates/jobs/source-build.yml index eb4b923a7777..d92860cba208 100644 --- a/eng/common/core-templates/jobs/source-build.yml +++ b/eng/common/core-templates/jobs/source-build.yml @@ -15,9 +15,6 @@ parameters: # one job runs on 'defaultManagedPlatform'. platforms: [] - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: '' # If set to true and running on a non-public project, @@ -34,7 +31,6 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ platform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} - ${{ if eq(length(parameters.platforms), 0) }}: @@ -43,5 +39,4 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ parameters.defaultManagedPlatform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index 77321eee11f7..acf16ed34963 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -11,10 +11,6 @@ parameters: # for details. The entire object is described in the 'job' template for simplicity, even though # the usage of the properties on this object is split between the 'job' and 'steps' templates. platform: {} - - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: false steps: diff --git a/eng/common/post-build/nuget-verification.ps1 b/eng/common/post-build/nuget-verification.ps1 index a365194a9389..ac5c69ffcac5 100644 --- a/eng/common/post-build/nuget-verification.ps1 +++ b/eng/common/post-build/nuget-verification.ps1 @@ -30,7 +30,7 @@ [CmdletBinding(PositionalBinding = $false)] param( [string]$NuGetExePath, - [string]$PackageSource = "https://api.nuget.org/v3/index.json", + [string]$PackageSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json", [string]$DownloadPath, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$args diff --git a/eng/dotnet-format/dotnet-format-integration.yml b/eng/dotnet-format/dotnet-format-integration.yml index 1fa8bbb8ad9f..25c17948bf59 100644 --- a/eng/dotnet-format/dotnet-format-integration.yml +++ b/eng/dotnet-format/dotnet-format-integration.yml @@ -2,6 +2,7 @@ parameters: - name: oneESCompat type: object default: + templateFolderName: templates publishTaskPrefix: '' - name: TestArguments @@ -63,6 +64,9 @@ parameters: - name: runtimeSourceProperties type: string default: '' +- name: populateInternalRuntimeVariables + type: boolean + default: false jobs: - job: Formatting_Check @@ -77,8 +81,11 @@ jobs: os: windows timeoutInMinutes: ${{ parameters.timeoutInMinutes }} steps: - - template: /eng/common/templates/steps/enable-internal-runtimes.yml - - template: /eng/common/templates/steps/enable-internal-sources.yml + - ${{ if eq(parameters.populateInternalRuntimeVariables, true) }}: + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-runtimes.yml - script: .\restore.cmd ${{ parameters.runtimeSourceProperties }} displayName: 🟣 Restore dependencies - script: | @@ -106,8 +113,11 @@ jobs: os: windows timeoutInMinutes: ${{ parameters.timeoutInMinutes }} steps: - - template: /eng/common/templates/steps/enable-internal-runtimes.yml - - template: /eng/common/templates/steps/enable-internal-sources.yml + - ${{ if eq(parameters.populateInternalRuntimeVariables, true) }}: + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-runtimes.yml - script: eng\dotnet-format\integration-test.cmd -repo '${{ testArgs._repo }}' -branchName '${{ testArgs._branchName }}' -sha '${{ testArgs._sha }}' -targetSolution '${{ testArgs._targetSolution }}' -useParentSdk ${{ testArgs._useParentSdk }} -testPath '$(Agent.TempDirectory)\temp' -stage 'prepare' -runtimeSourceProperties '${{ parameters.runtimeSourceProperties }}' displayName: 🟣 Prepare ${{ testArgs._repoName }} for formatting diff --git a/eng/pipelines/templates/jobs/sdk-build.yml b/eng/pipelines/templates/jobs/sdk-build.yml index 202d480959af..a9fc3537bfcb 100644 --- a/eng/pipelines/templates/jobs/sdk-build.yml +++ b/eng/pipelines/templates/jobs/sdk-build.yml @@ -12,7 +12,9 @@ parameters: testProjects: $(Build.SourcesDirectory)/test/UnitTests.proj publishRetryConfig: false publishXunitResults: false + testRunnerAdditionalArguments: '' enableSbom: true + populateInternalRuntimeVariables: false timeoutInMinutes: 150 ### ENV VARS ### testFullMSBuild: false @@ -71,11 +73,11 @@ jobs: targetPath: $(Build.SourcesDirectory)/eng/BuildConfiguration artifactName: BuildConfiguration - # Populate internal runtime variables. - - template: /eng/common/templates/steps/enable-internal-sources.yml - parameters: - legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - - template: /eng/common/templates/steps/enable-internal-runtimes.yml + - ${{ if eq(parameters.populateInternalRuntimeVariables, true) }}: + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + - template: /eng/common/${{ parameters.oneESCompat.templateFolderName }}/steps/enable-internal-runtimes.yml - ${{ if eq(parameters.downloadManifestMsiPackages, true) }}: - task: DownloadBuildArtifacts@1 @@ -84,7 +86,7 @@ jobs: downloadPath: $(Build.SourcesDirectory)/artifacts/downloaded-manifest-msi-packages itemPattern: '**/*Manifest-*.Msi.*.nupkg' checkDownloadedFiles: true - displayName: Download Manifest msi packages + displayName: 🟣 Download Manifest MSI Packages ############### BUILDING ############### - ${{ if eq(parameters.pool.os, 'windows') }}: @@ -105,10 +107,6 @@ jobs: BuildConfig: $(buildConfiguration) TestFullMSBuild: ${{ parameters.testFullMSBuild }} - - ${{ if eq(parameters.targetArchitecture, 'x64') }}: - - powershell: build/RunTestTemplateTests.ps1 - displayName: 🟣 Run Test Templates Tests - - ${{ else }}: - script: | source $(Build.SourcesDirectory)/eng/common/native/init-os-and-arch.sh @@ -141,12 +139,14 @@ jobs: - script: $(Build.SourcesDirectory)/artifacts/bin/redist/$(buildConfiguration)/dotnet/dotnet workload install wasm-tools --skip-manifest-update workingDirectory: $(Build.SourcesDirectory)/artifacts/bin displayName: 🟣 Install wasm-tools Workload + # For the /p:Projects syntax for PowerShell, see: https://github.com/dotnet/msbuild/issues/471#issuecomment-1146466335 - ${{ if eq(parameters.pool.os, 'windows') }}: - powershell: eng/common/build.ps1 -restore -test -ci -prepareMachine -nativeToolsOnMachine -configuration $(buildConfiguration) /p:Projects=\`"${{ replace(parameters.testProjects, ';', '`;') }}\`" + /p:TestRunnerAdditionalArguments="${{ parameters.testRunnerAdditionalArguments }}" /p:TargetArchitecture=${{ parameters.targetArchitecture }} ${{ parameters.runtimeSourceProperties }} /p:CustomHelixTargetQueue=${{ parameters.helixTargetQueue }} @@ -159,6 +159,7 @@ jobs: HelixAccessToken: $(HelixApiAccessToken) RunAoTTests: ${{ parameters.runAoTTests }} TestFullMSBuild: ${{ parameters.testFullMSBuild }} + - ${{ else }}: # For the /p:Projects syntax for Bash, see: https://github.com/dotnet/msbuild/issues/471#issuecomment-1690189034 # The /p:CustomHelixTargetQueue syntax is: @ @@ -167,6 +168,7 @@ jobs: -restore -test -ci -prepareMachine -configuration $(buildConfiguration) '/p:Projects="${{ parameters.testProjects }}"' + /p:TestRunnerAdditionalArguments="${{ parameters.testRunnerAdditionalArguments }}" /p:TargetArchitecture=${{ parameters.targetArchitecture }} /p:TargetRid=${{ parameters.runtimeIdentifier }} ${{ parameters.osProperties }} diff --git a/eng/pipelines/templates/jobs/sdk-job-matrix.yml b/eng/pipelines/templates/jobs/sdk-job-matrix.yml index 8e9000f3c6cc..870f20d8f88b 100644 --- a/eng/pipelines/templates/jobs/sdk-job-matrix.yml +++ b/eng/pipelines/templates/jobs/sdk-job-matrix.yml @@ -31,7 +31,6 @@ parameters: container: azureLinux30Amd64 helixTargetContainer: $(helixTargetContainerPrefix)ubuntu-24.04-helix-amd64 osProperties: /p:OSName=linux /p:BuildSdkDeb=true - runTests: true # Helix is hanging on this job using the container. See: https://github.com/dotnet/dnceng/issues/6000 disableJob: true - categoryName: TemplateEngine diff --git a/es-metadata.yml b/es-metadata.yml new file mode 100644 index 000000000000..af486ad81d2f --- /dev/null +++ b/es-metadata.yml @@ -0,0 +1,8 @@ +schemaVersion: 0.0.1 +isProduction: true +accountableOwners: + service: 30f635d8-2918-48af-8ddf-d9bc854b7584 +routing: + defaultAreaPath: + org: devdiv + path: DevDiv\NET Tools\SDK diff --git a/global.json b/global.json index 1b17260e86c1..cf7985786d3b 100644 --- a/global.json +++ b/global.json @@ -7,7 +7,7 @@ "errorMessage": "The .NET SDK is not installed or is not configured correctly. Please run ./build to install the correct SDK version locally." }, "tools": { - "dotnet": "10.0.100-rc.1.25420.111", + "dotnet": "10.0.100-rc.1.25451.107", "runtimes": { "dotnet": [ "$(MicrosoftNETCorePlatformsPackageVersion)" @@ -21,8 +21,8 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25460.104", - "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25460.104", + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25509.106", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25509.106", "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.4.0", "Microsoft.WixToolset.Sdk": "5.0.2-dotnet.2737382" diff --git a/sdk.slnx b/sdk.slnx index 0246a040f447..dc0633c42d02 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -312,6 +312,7 @@ + diff --git a/src/BuiltInTools/AspireService/AspireServerService.cs b/src/BuiltInTools/AspireService/AspireServerService.cs index bf2f8f341b81..064de0ee7a50 100644 --- a/src/BuiltInTools/AspireService/AspireServerService.cs +++ b/src/BuiltInTools/AspireService/AspireServerService.cs @@ -123,6 +123,7 @@ public List> GetServerConnectionEnvironment() new(DebugSessionServerCertEnvVar, _certificateEncodedBytes), ]; + /// public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelationToken) => SendNotificationAsync( new SessionTerminatedNotification() @@ -136,6 +137,7 @@ public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int pro sessionId, cancelationToken); + /// public ValueTask NotifySessionStartedAsync(string dcpId, string sessionId, int processId, CancellationToken cancelationToken) => SendNotificationAsync( new ProcessRestartedNotification() @@ -148,6 +150,7 @@ public ValueTask NotifySessionStartedAsync(string dcpId, string sessionId, int p sessionId, cancelationToken); + /// public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isStdErr, string data, CancellationToken cancelationToken) => SendNotificationAsync( new ServiceLogsNotification() @@ -161,23 +164,28 @@ public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isSt sessionId, cancelationToken); - private async ValueTask SendNotificationAsync(TNotification notification, string dcpId, string sessionId, CancellationToken cancelationToken) + /// + private async ValueTask SendNotificationAsync(TNotification notification, string dcpId, string sessionId, CancellationToken cancellationToken) where TNotification : SessionNotification { try { - Log($"[#{sessionId}] Sending '{notification.NotificationType}'"); + Log($"[#{sessionId}] Sending '{notification.NotificationType}': {notification}"); var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(notification, JsonSerializerOptions); - await SendMessageAsync(dcpId, jsonSerialized, cancelationToken); - } - catch (Exception e) when (e is not OperationCanceledException && LogAndPropagate(e)) - { - } + var success = await SendMessageAsync(dcpId, jsonSerialized, cancellationToken); - bool LogAndPropagate(Exception e) + if (!success) + { + cancellationToken.ThrowIfCancellationRequested(); + Log($"[#{sessionId}] Failed to send message: Connection not found (dcpId='{dcpId}')."); + } + } + catch (Exception e) when (e is not OperationCanceledException) { - Log($"[#{sessionId}] Sending '{notification.NotificationType}' failed: {e.Message}"); - return false; + if (!cancellationToken.IsCancellationRequested) + { + Log($"[#{sessionId}] Failed to send message: {e.Message}"); + } } } @@ -373,15 +381,13 @@ private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, b } } - private async Task SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken) + private async ValueTask SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken) { // Find the connection for the passed in dcpId WebSocketConnection? connection = _socketConnectionManager.GetSocketConnection(dcpId); if (connection is null) { - // Most likely the connection has already gone away - Log($"Send message failure: Connection with the following dcpId was not found {dcpId}"); - return; + return false; } var success = false; @@ -405,6 +411,8 @@ private async Task SendMessageAsync(string dcpId, byte[] messageBytes, Cancellat _webSocketAccess.Release(); } + + return success; } private async ValueTask HandleStopSessionRequestAsync(HttpContext context, string sessionId) diff --git a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs index 579fe6b4a88f..d4213593cf5a 100644 --- a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs +++ b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs @@ -56,6 +56,9 @@ internal sealed class SessionTerminatedNotification : SessionNotification [Required] [JsonPropertyName("exit_code")] public required int? ExitCode { get; init; } + + public override string ToString() + => $"pid={Pid}, exit_code={ExitCode}"; } /// @@ -70,6 +73,9 @@ internal sealed class ProcessRestartedNotification : SessionNotification [Required] [JsonPropertyName("pid")] public required int PID { get; init; } + + public override string ToString() + => $"pid={PID}"; } /// @@ -91,4 +97,7 @@ internal sealed class ServiceLogsNotification : SessionNotification [Required] [JsonPropertyName("log_message")] public required string LogMessage { get; init; } + + public override string ToString() + => $"log_message='{LogMessage}', is_std_err={IsStdErr}"; } diff --git a/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj b/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj index c8835d1acb42..84852c4df9a9 100644 --- a/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj +++ b/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs b/src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs index d596594e551d..6110f0fa631c 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/PipeListener.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.IO.Pipes; using System.Reflection; using System.Runtime.Loader; @@ -9,6 +10,17 @@ namespace Microsoft.DotNet.HotReload; internal sealed class PipeListener(string pipeName, IHotReloadAgent agent, Action log, int connectionTimeoutMS = 5000) { + /// + /// Messages to the client sent after the initial is sent + /// need to be sent while holding this lock in order to synchronize + /// 1) responses to requests received from the client (e.g. ) or + /// 2) notifications sent to the client that may be triggered at arbitrary times (e.g. ). + /// + private readonly SemaphoreSlim _messageToClientLock = new(initialCount: 1); + + // Not-null once initialized: + private NamedPipeClientStream? _pipeClient; + public Task Listen(CancellationToken cancellationToken) { // Connect to the pipe synchronously. @@ -21,23 +33,23 @@ public Task Listen(CancellationToken cancellationToken) log($"Connecting to hot-reload server via pipe {pipeName}"); - var pipeClient = new NamedPipeClientStream(serverName: ".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); + _pipeClient = new NamedPipeClientStream(serverName: ".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); try { - pipeClient.Connect(connectionTimeoutMS); + _pipeClient.Connect(connectionTimeoutMS); log("Connected."); } catch (TimeoutException) { log($"Failed to connect in {connectionTimeoutMS}ms."); - pipeClient.Dispose(); + _pipeClient.Dispose(); return Task.CompletedTask; } try { // block execution of the app until initial updates are applied: - InitializeAsync(pipeClient, cancellationToken).GetAwaiter().GetResult(); + InitializeAsync(cancellationToken).GetAwaiter().GetResult(); } catch (Exception e) { @@ -46,7 +58,7 @@ public Task Listen(CancellationToken cancellationToken) log(e.Message); } - pipeClient.Dispose(); + _pipeClient.Dispose(); agent.Dispose(); return Task.CompletedTask; @@ -56,7 +68,7 @@ public Task Listen(CancellationToken cancellationToken) { try { - await ReceiveAndApplyUpdatesAsync(pipeClient, initialUpdates: false, cancellationToken); + await ReceiveAndApplyUpdatesAsync(initialUpdates: false, cancellationToken); } catch (Exception e) when (e is not OperationCanceledException) { @@ -64,40 +76,44 @@ public Task Listen(CancellationToken cancellationToken) } finally { - pipeClient.Dispose(); + _pipeClient.Dispose(); agent.Dispose(); } }, cancellationToken); } - private async Task InitializeAsync(NamedPipeClientStream pipeClient, CancellationToken cancellationToken) + private async Task InitializeAsync(CancellationToken cancellationToken) { + Debug.Assert(_pipeClient != null); + agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose); var initPayload = new ClientInitializationResponse(agent.Capabilities); - await initPayload.WriteAsync(pipeClient, cancellationToken); + await initPayload.WriteAsync(_pipeClient, cancellationToken); // Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules. // We should only receive ManagedCodeUpdate when when the debugger isn't attached, // otherwise the initialization should send InitialUpdatesCompleted immediately. // The debugger itself applies these updates when launching process with the debugger attached. - await ReceiveAndApplyUpdatesAsync(pipeClient, initialUpdates: true, cancellationToken); + await ReceiveAndApplyUpdatesAsync(initialUpdates: true, cancellationToken); } - private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, bool initialUpdates, CancellationToken cancellationToken) + private async Task ReceiveAndApplyUpdatesAsync(bool initialUpdates, CancellationToken cancellationToken) { - while (pipeClient.IsConnected) + Debug.Assert(_pipeClient != null); + + while (_pipeClient.IsConnected) { - var payloadType = (RequestType)await pipeClient.ReadByteAsync(cancellationToken); + var payloadType = (RequestType)await _pipeClient.ReadByteAsync(cancellationToken); switch (payloadType) { case RequestType.ManagedCodeUpdate: - await ReadAndApplyManagedCodeUpdateAsync(pipeClient, cancellationToken); + await ReadAndApplyManagedCodeUpdateAsync(cancellationToken); break; case RequestType.StaticAssetUpdate: - await ReadAndApplyStaticAssetUpdateAsync(pipeClient, cancellationToken); + await ReadAndApplyStaticAssetUpdateAsync(cancellationToken); break; case RequestType.InitialUpdatesCompleted when initialUpdates: @@ -110,11 +126,11 @@ private async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, } } - private async ValueTask ReadAndApplyManagedCodeUpdateAsync( - NamedPipeClientStream pipeClient, - CancellationToken cancellationToken) + private async ValueTask ReadAndApplyManagedCodeUpdateAsync(CancellationToken cancellationToken) { - var request = await ManagedCodeUpdateRequest.ReadAsync(pipeClient, cancellationToken); + Debug.Assert(_pipeClient != null); + + var request = await ManagedCodeUpdateRequest.ReadAsync(_pipeClient, cancellationToken); bool success; try @@ -131,15 +147,14 @@ private async ValueTask ReadAndApplyManagedCodeUpdateAsync( var logEntries = agent.Reporter.GetAndClearLogEntries(request.ResponseLoggingLevel); - var response = new UpdateResponse(logEntries, success); - await response.WriteAsync(pipeClient, cancellationToken); + await SendResponseAsync(new UpdateResponse(logEntries, success), cancellationToken); } - private async ValueTask ReadAndApplyStaticAssetUpdateAsync( - NamedPipeClientStream pipeClient, - CancellationToken cancellationToken) + private async ValueTask ReadAndApplyStaticAssetUpdateAsync(CancellationToken cancellationToken) { - var request = await StaticAssetUpdateRequest.ReadAsync(pipeClient, cancellationToken); + Debug.Assert(_pipeClient != null); + + var request = await StaticAssetUpdateRequest.ReadAsync(_pipeClient, cancellationToken); try { @@ -155,8 +170,22 @@ private async ValueTask ReadAndApplyStaticAssetUpdateAsync( // Updating static asset only invokes ContentUpdate metadata update handlers. // Failures of these handlers are reported to the log and ignored. // Therefore, this request always succeeds. - var response = new UpdateResponse(logEntries, success: true); + await SendResponseAsync(new UpdateResponse(logEntries, success: true), cancellationToken); + } - await response.WriteAsync(pipeClient, cancellationToken); + internal async ValueTask SendResponseAsync(T response, CancellationToken cancellationToken) + where T : IResponse + { + Debug.Assert(_pipeClient != null); + try + { + await _messageToClientLock.WaitAsync(cancellationToken); + await _pipeClient.WriteAsync((byte)response.Type, cancellationToken); + await response.WriteAsync(_pipeClient, cancellationToken); + } + finally + { + _messageToClientLock.Release(); + } } } diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs index 28890852482f..03c7b04a4fea 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -40,14 +40,49 @@ public static void Initialize() RegisterSignalHandlers(); - var agent = new HotReloadAgent(assemblyResolvingHandler: (_, args) => - { - Log($"Resolving '{args.Name}, Version={args.Version}'"); - var path = Path.Combine(processDir, args.Name + ".dll"); - return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null; - }); + PipeListener? listener = null; + + var agent = new HotReloadAgent( + assemblyResolvingHandler: (_, args) => + { + Log($"Resolving '{args.Name}, Version={args.Version}'"); + var path = Path.Combine(processDir, args.Name + ".dll"); + return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null; + }, + hotReloadExceptionCreateHandler: (code, message) => + { + // Continue executing the code if the debugger is attached. + // It will throw the exception and the debugger will handle it. + if (Debugger.IsAttached) + { + return; + } + + Debug.Assert(listener != null); + Log($"Runtime rude edit detected: '{message}'"); + + SendAndForgetAsync().Wait(); + + // Handle Ctrl+C to terminate gracefully: + Console.CancelKeyPress += (_, _) => Environment.Exit(0); + + // wait for the process to be terminated by the Hot Reload client (other threads might still execute): + Thread.Sleep(Timeout.Infinite); + + async Task SendAndForgetAsync() + { + try + { + await listener.SendResponseAsync(new HotReloadExceptionCreatedNotification(code, message), CancellationToken.None); + } + catch + { + // do not crash the app + } + } + }); - var listener = new PipeListener(s_namedPipeName, agent, Log); + listener = new PipeListener(s_namedPipeName, agent, Log); // fire and forget: _ = listener.Listen(CancellationToken.None); diff --git a/src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs b/src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs index 66766e1c74e2..dfa0158c53cd 100644 --- a/src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs +++ b/src/BuiltInTools/HotReloadAgent.PipeRpc/NamedPipeContract.cs @@ -12,23 +12,39 @@ namespace Microsoft.DotNet.HotReload; -internal interface IRequest +internal interface IMessage { - RequestType Type { get; } ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken); } +internal interface IRequest : IMessage +{ + RequestType Type { get; } +} + +internal interface IResponse : IMessage +{ + ResponseType Type { get; } +} + internal interface IUpdateRequest : IRequest { } -internal enum RequestType +internal enum RequestType : byte { ManagedCodeUpdate = 1, StaticAssetUpdate = 2, InitialUpdatesCompleted = 3, } +internal enum ResponseType : byte +{ + InitializationResponse = 1, + UpdateResponse = 2, + HotReloadExceptionNotification = 3, +} + internal readonly struct ManagedCodeUpdateRequest(IReadOnlyList updates, ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest { private const byte Version = 4; @@ -81,8 +97,10 @@ public static async ValueTask ReadAsync(Stream stream, } } -internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success) +internal readonly struct UpdateResponse(IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, bool success) : IResponse { + public ResponseType Type => ResponseType.UpdateResponse; + public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) { await stream.WriteAsync(success, cancellationToken); @@ -116,10 +134,12 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT } } -internal readonly struct ClientInitializationResponse(string capabilities) +internal readonly struct ClientInitializationResponse(string capabilities) : IResponse { private const byte Version = 0; + public ResponseType Type => ResponseType.InitializationResponse; + public string Capabilities { get; } = capabilities; public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) @@ -141,6 +161,26 @@ public static async ValueTask ReadAsync(Stream str } } +internal readonly struct HotReloadExceptionCreatedNotification(int code, string message) : IResponse +{ + public ResponseType Type => ResponseType.HotReloadExceptionNotification; + public int Code => code; + public string Message => message; + + public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) + { + await stream.WriteAsync(code, cancellationToken); + await stream.WriteAsync(message, cancellationToken); + } + + public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var code = await stream.ReadInt32Async(cancellationToken); + var message = await stream.ReadStringAsync(cancellationToken); + return new HotReloadExceptionCreatedNotification(code, message); + } +} + internal readonly struct StaticAssetUpdateRequest( RuntimeStaticAssetUpdate update, ResponseLoggingLevel responseLoggingLevel) : IUpdateRequest diff --git a/src/BuiltInTools/HotReloadAgent.WebAssembly.Browser/WebAssemblyHotReload.cs b/src/BuiltInTools/HotReloadAgent.WebAssembly.Browser/WebAssemblyHotReload.cs index 2169ef3dbd9b..1aad8e47f072 100644 --- a/src/BuiltInTools/HotReloadAgent.WebAssembly.Browser/WebAssemblyHotReload.cs +++ b/src/BuiltInTools/HotReloadAgent.WebAssembly.Browser/WebAssemblyHotReload.cs @@ -71,7 +71,8 @@ public static async Task InitializeAsync(string baseUri) { s_initialized = true; - var agent = new HotReloadAgent(assemblyResolvingHandler: null); + // TODO: Implement hotReloadExceptionCreateHandler: https://github.com/dotnet/sdk/issues/51056 + var agent = new HotReloadAgent(assemblyResolvingHandler: null, hotReloadExceptionCreateHandler: null); var existingAgent = Interlocked.CompareExchange(ref s_hotReloadAgent, agent, null); if (existingAgent != null) diff --git a/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs b/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs index 4114bed5e6fd..f64180144078 100644 --- a/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs +++ b/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs @@ -38,11 +38,16 @@ internal sealed class HotReloadAgent : IDisposable, IHotReloadAgent private Func? _assemblyResolvingHandlerToInstall; private Func? _installedAssemblyResolvingHandler; - public HotReloadAgent(Func? assemblyResolvingHandler) + // handler to install to HotReloadException.Created: + private Action? _hotReloadExceptionCreateHandler; + + public HotReloadAgent( + Func? assemblyResolvingHandler, + Action? hotReloadExceptionCreateHandler) { _metadataUpdateHandlerInvoker = new(Reporter); _assemblyResolvingHandlerToInstall = assemblyResolvingHandler; - + _hotReloadExceptionCreateHandler = hotReloadExceptionCreateHandler; GetUpdaterMethodsAndCapabilities(out _applyUpdate, out _capabilities); AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; @@ -148,11 +153,69 @@ public void ApplyManagedCodeUpdates(IEnumerable update cachedModuleUpdates.Add(update); } - _metadataUpdateHandlerInvoker.MetadataUpdated(GetMetadataUpdateTypes(updates)); + var updatedTypes = GetMetadataUpdateTypes(updates); + + InstallHotReloadExceptionCreatedHandler(updatedTypes); + + _metadataUpdateHandlerInvoker.MetadataUpdated(updatedTypes); Reporter.Report("Updates applied.", AgentMessageSeverity.Verbose); } + private void InstallHotReloadExceptionCreatedHandler(Type[] types) + { + if (_hotReloadExceptionCreateHandler is null) + { + // already installed or not available + return; + } + + var exceptionType = types.FirstOrDefault(static t => t.FullName == "System.Runtime.CompilerServices.HotReloadException"); + if (exceptionType == null) + { + return; + } + + var handler = Interlocked.Exchange(ref _hotReloadExceptionCreateHandler, null); + if (handler == null) + { + // already installed or not available + return; + } + + // HotReloadException has a private static field Action Created, unless emitted by previous versions of the compiler: + // See https://github.com/dotnet/roslyn/blob/06f2643e1268e4a7fcdf1221c052f9c8cce20b60/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedHotReloadExceptionSymbol.cs#L29 + var createdField = exceptionType.GetField("Created", BindingFlags.Static | BindingFlags.NonPublic); + var codeField = exceptionType.GetField("Code", BindingFlags.Public | BindingFlags.Instance); + if (createdField == null || codeField == null) + { + Reporter.Report($"Failed to install HotReloadException handler: not supported by the compiler", AgentMessageSeverity.Verbose); + return; + } + + try + { + createdField.SetValue(null, new Action(e => + { + try + { + handler(codeField.GetValue(e) is int code ? code : 0, e.Message); + } + catch + { + // do not crash the app + } + })); + } + catch (Exception e) + { + Reporter.Report($"Failed to install HotReloadException handler: {e.Message}", AgentMessageSeverity.Verbose); + return; + } + + Reporter.Report($"HotReloadException handler installed.", AgentMessageSeverity.Verbose); + } + private Type[] GetMetadataUpdateTypes(IEnumerable updates) { List? types = null; diff --git a/src/BuiltInTools/HotReloadAgent/MetadataUpdateHandlerInvoker.cs b/src/BuiltInTools/HotReloadAgent/MetadataUpdateHandlerInvoker.cs index 952ba4866def..e841af26513e 100644 --- a/src/BuiltInTools/HotReloadAgent/MetadataUpdateHandlerInvoker.cs +++ b/src/BuiltInTools/HotReloadAgent/MetadataUpdateHandlerInvoker.cs @@ -110,7 +110,7 @@ private RegisteredActions GetActions() } /// - /// Invokes all registered mtadata update handlers. + /// Invokes all registered metadata update handlers. /// internal void MetadataUpdated(Type[] updatedTypes) { diff --git a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs index 7587b7623117..c7b325a4c348 100644 --- a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs @@ -28,6 +28,9 @@ internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger private NamedPipeServerStream? _pipe; private bool _managedCodeUpdateFailedOrCancelled; + // The status of the last update response. + private TaskCompletionSource _updateStatusSource = new(); + public override void Dispose() { DisposePipe(); @@ -35,9 +38,14 @@ public override void Dispose() private void DisposePipe() { - Logger.LogDebug("Disposing agent communication pipe"); - _pipe?.Dispose(); - _pipe = null; + if (_pipe != null) + { + Logger.LogDebug("Disposing agent communication pipe"); + + // Dispose the pipe but do not set it to null, so that any in-progress + // operations throw the appropriate exception type. + _pipe.Dispose(); + } } // for testing @@ -70,24 +78,65 @@ async Task> ConnectAsync() var capabilities = (await ClientInitializationResponse.ReadAsync(_pipe, cancellationToken)).Capabilities; Logger.Log(LogEvents.Capabilities, capabilities); + + // fire and forget: + _ = ListenForResponsesAsync(cancellationToken); + return [.. capabilities.Split(' ')]; } - catch (EndOfStreamException) + catch (Exception e) when (e is not OperationCanceledException) { - // process terminated before capabilities sent: + ReportPipeReadException(e, "capabilities", cancellationToken); return []; } - catch (Exception e) when (e is not OperationCanceledException) + } + } + + private void ReportPipeReadException(Exception e, string responseType, CancellationToken cancellationToken) + { + // Don't report a warning when cancelled or the pipe has been disposed. The process has terminated or the host is shutting down in that case. + // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. + if (e is ObjectDisposedException or EndOfStreamException || cancellationToken.IsCancellationRequested) + { + return; + } + + Logger.LogError("Failed to read {ResponseType} from the pipe: {Message}", responseType, e.Message); + } + + private async Task ListenForResponsesAsync(CancellationToken cancellationToken) + { + Debug.Assert(_pipe != null); + + try + { + while (!cancellationToken.IsCancellationRequested) { - // pipe might throw another exception when forcibly closed on process termination: - if (!cancellationToken.IsCancellationRequested) + var type = (ResponseType)await _pipe.ReadByteAsync(cancellationToken); + + switch (type) { - Logger.LogError("Failed to read capabilities: {Message}", e.Message); + case ResponseType.UpdateResponse: + // update request can't be issued again until the status is read and a new source is created: + _updateStatusSource.SetResult(await ReadUpdateResponseAsync(cancellationToken)); + break; + + case ResponseType.HotReloadExceptionNotification: + var notification = await HotReloadExceptionCreatedNotification.ReadAsync(_pipe, cancellationToken); + RuntimeRudeEditDetected(notification.Code, notification.Message); + break; + + default: + // can't continue, the pipe is in undefined state: + Logger.LogError("Unexpected response received from the agent: {ResponseType}", type); + return; } - - return []; } } + catch (Exception e) + { + ReportPipeReadException(e, "response", cancellationToken); + } } [MemberNotNull(nameof(_capabilitiesTask))] @@ -101,8 +150,7 @@ private void RequireReadyForUpdates() // should only be called after connection has been created: _ = GetCapabilitiesTask(); - if (_pipe == null) - throw new InvalidOperationException("Pipe has been disposed."); + Debug.Assert(_pipe != null); } public override void ConfigureLaunchEnvironment(IDictionary environmentBuilder) @@ -152,7 +200,13 @@ public override async Task ApplyManagedCodeUpdatesAsync(ImmutableAr { if (!success) { - Logger.LogWarning("Further changes won't be applied to this process."); + // Don't report a warning when cancelled. The process has terminated or the host is shutting down in that case. + // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. + if (!cancellationToken.IsCancellationRequested) + { + Logger.LogWarning("Further changes won't be applied to this process."); + } + _managedCodeUpdateFailedOrCancelled = true; DisposePipe(); } @@ -216,7 +270,7 @@ public async override Task ApplyStaticAssetUpdatesAsync(ImmutableAr private ValueTask SendAndReceiveUpdateAsync(TRequest request, bool isProcessSuspended, CancellationToken cancellationToken) where TRequest : IUpdateRequest { - // Should not be disposed: + // Should not initialized: Debug.Assert(_pipe != null); return SendAndReceiveUpdateAsync( @@ -241,8 +295,10 @@ async ValueTask SendAndReceiveAsync(int batchId, CancellationToken cancell Logger.LogDebug("Update batch #{UpdateId} failed.", batchId); } - catch (Exception e) when (e is not OperationCanceledException || isProcessSuspended) + catch (Exception e) { + // Don't report an error when cancelled. The process has terminated or the host is shutting down in that case. + // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. if (cancellationToken.IsCancellationRequested) { Logger.LogDebug("Update batch #{UpdateId} canceled.", batchId); @@ -267,7 +323,14 @@ async ValueTask WriteRequestAsync(CancellationToken cancellationToken) private async ValueTask ReceiveUpdateResponseAsync(CancellationToken cancellationToken) { - // Should not be disposed: + var result = await _updateStatusSource.Task; + _updateStatusSource = new TaskCompletionSource(); + return result; + } + + private async ValueTask ReadUpdateResponseAsync(CancellationToken cancellationToken) + { + // Should be initialized: Debug.Assert(_pipe != null); var (success, log) = await UpdateResponse.ReadAsync(_pipe, cancellationToken); @@ -296,10 +359,12 @@ public override async Task InitialUpdatesAppliedAsync(CancellationToken cancella } catch (Exception e) when (e is not OperationCanceledException) { - // pipe might throw another exception when forcibly closed on process termination: + // Pipe might throw another exception when forcibly closed on process termination. + // Don't report an error when cancelled. The process has terminated or the host is shutting down in that case. + // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. if (!cancellationToken.IsCancellationRequested) { - Logger.LogError("Failed to send InitialUpdatesCompleted: {Message}", e.Message); + Logger.LogError("Failed to send {RequestType}: {Message}", nameof(RequestType.InitialUpdatesCompleted), e.Message); } } } diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs index fe14176a8b6e..2a563419e4f2 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs @@ -32,6 +32,11 @@ internal abstract class HotReloadClient(ILogger logger, ILogger agentLogger) : I private readonly object _pendingUpdatesGate = new(); private Task _pendingUpdates = Task.CompletedTask; + /// + /// Invoked when a rude edit is detected at runtime. + /// + public event Action? OnRuntimeRudeEdit; + // for testing internal Task PendingUpdates => _pendingUpdates; @@ -41,25 +46,47 @@ internal Task PendingUpdates /// /// Initiates connection with the agent in the target process. /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract void InitiateConnection(CancellationToken cancellationToken); /// /// Waits until the connection with the agent is established. /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract Task WaitForConnectionEstablishedAsync(CancellationToken cancellationToken); + /// + /// Returns update capabilities of the target process. + /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract Task> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken); + /// + /// Applies managed code updates to the target process. + /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract Task ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken); + + /// + /// Applies static asset updates to the target process. + /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract Task ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken); /// /// Notifies the agent that the initial set of updates has been applied and the user code in the process can start executing. /// + /// Cancellation token. The cancellation should trigger on process terminatation. public abstract Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken); + /// + /// Disposes the client. Can occur unexpectedly whenever the process exits. + /// public abstract void Dispose(); + protected void RuntimeRudeEditDetected(int errorCode, string message) + => OnRuntimeRudeEdit?.Invoke(errorCode, message); + public static void ReportLogEntry(ILogger logger, string message, AgentMessageSeverity severity) { var level = severity switch @@ -72,7 +99,7 @@ public static void ReportLogEntry(ILogger logger, string message, AgentMessageSe logger.Log(level, message); } - public async Task> FilterApplicableUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) + protected async Task> FilterApplicableUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) { var availableCapabilities = await GetUpdateCapabilitiesAsync(cancellationToken); var applicableUpdates = new List(); diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs index bd0a4be6799c..fcf541045fc6 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs @@ -23,6 +23,9 @@ public HotReloadClients(HotReloadClient client, AbstractBrowserRefreshServer? br { } + /// + /// Disposes all clients. Can occur unexpectedly whenever the process exits. + /// public void Dispose() { foreach (var (client, _) in clients) @@ -34,6 +37,28 @@ public void Dispose() public AbstractBrowserRefreshServer? BrowserRefreshServer => browserRefreshServer; + /// + /// Invoked when a rude edit is detected at runtime. + /// May be invoked multiple times, by each client. + /// + public event Action OnRuntimeRudeEdit + { + add + { + foreach (var (client, _) in clients) + { + client.OnRuntimeRudeEdit += value; + } + } + remove + { + foreach (var (client, _) in clients) + { + client.OnRuntimeRudeEdit -= value; + } + } + } + /// /// All clients share the same loggers. /// @@ -56,6 +81,7 @@ internal void ConfigureLaunchEnvironment(IDictionary environment browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder, enableHotReload: true); } + /// Cancellation token. The cancellation should trigger on process terminatation. internal void InitiateConnection(CancellationToken cancellationToken) { foreach (var (client, _) in clients) @@ -64,11 +90,13 @@ internal void InitiateConnection(CancellationToken cancellationToken) } } + /// Cancellation token. The cancellation should trigger on process terminatation. internal async ValueTask WaitForConnectionEstablishedAsync(CancellationToken cancellationToken) { await Task.WhenAll(clients.Select(c => c.client.WaitForConnectionEstablishedAsync(cancellationToken))); } + /// Cancellation token. The cancellation should trigger on process terminatation. public async ValueTask> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken) { if (clients is [var (singleClient, _)]) @@ -83,7 +111,9 @@ public async ValueTask> GetUpdateCapabilitiesAsync(Cancel return [.. results.SelectMany(r => r).Distinct(StringComparer.Ordinal).OrderBy(c => c)]; } - public async ValueTask ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + /// Cancellation token. The cancellation should trigger on process terminatation. + /// True if the updates are initial updates applied automatically when a process starts. + public async ValueTask ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, bool isInitial, CancellationToken cancellationToken) { var anyFailure = false; @@ -128,9 +158,13 @@ public async ValueTask ApplyManagedCodeUpdatesAsync(ImmutableArrayCancellation token. The cancellation should trigger on process terminatation. public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellationToken) { if (clients is [var (singleClient, _)]) @@ -151,6 +186,7 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation } } + /// Cancellation token. The cancellation should trigger on process terminatation. public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)> assets, CancellationToken cancellationToken) { if (browserRefreshServer != null) @@ -173,7 +209,7 @@ public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, str #endif content = ImmutableCollectionsMarshal.AsImmutableArray(blob); } - catch (Exception e) + catch (Exception e) when (e is not OperationCanceledException) { ClientLogger.LogError("Failed to read file {FilePath}: {Message}", filePath, e.Message); continue; @@ -190,6 +226,7 @@ public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, str } } + /// Cancellation token. The cancellation should trigger on process terminatation. public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) { if (clients is [var (singleClient, _)]) @@ -202,6 +239,7 @@ public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArrayCancellation token. The cancellation should trigger on process terminatation. public ValueTask ReportCompilationErrorsInApplicationAsync(ImmutableArray compilationErrors, CancellationToken cancellationToken) => browserRefreshServer?.ReportCompilationErrorsInBrowserAsync(compilationErrors, cancellationToken) ?? ValueTask.CompletedTask; } diff --git a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs index a74e5097a6d5..7be92781e01c 100644 --- a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs +++ b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs @@ -25,9 +25,11 @@ public static void Log(this ILogger logger, LogEvent logEvent, params object[] a public static readonly LogEvent HotReloadSucceeded = Create(LogLevel.Information, "Hot reload succeeded."); public static readonly LogEvent RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser."); public static readonly LogEvent ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser."); + public static readonly LogEvent SendingWaitMessage = Create(LogLevel.Debug, "Sending wait message."); public static readonly LogEvent NoBrowserConnected = Create(LogLevel.Debug, "No browser is connected."); public static readonly LogEvent FailedToReceiveResponseFromConnectedBrowser = Create(LogLevel.Debug, "Failed to receive response from a connected browser."); public static readonly LogEvent UpdatingDiagnostics = Create(LogLevel.Debug, "Updating diagnostics."); public static readonly LogEvent SendingStaticAssetUpdateRequest = Create(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'."); public static readonly LogEvent RefreshServerRunningAt = Create(LogLevel.Debug, "Refresh server running at {0}."); + public static readonly LogEvent ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server."); } diff --git a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs index eda15d200f96..91d99296db0b 100644 --- a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs +++ b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs @@ -101,7 +101,7 @@ public void ConfigureLaunchEnvironment(IDictionary builder, bool builder[MiddlewareEnvironmentVariables.DotNetModifiableAssemblies] = "debug"; } - if (logger.IsEnabled(LogLevel.Debug)) + if (logger.IsEnabled(LogLevel.Trace)) { // enable debug logging from middleware: builder[MiddlewareEnvironmentVariables.LoggingLevel] = "Debug"; @@ -163,7 +163,7 @@ public async Task WaitForClientConnectionAsync(CancellationToken cancellationTok }, progressCancellationSource.Token); // Work around lack of Task.WaitAsync(cancellationToken) on .NET Framework: - cancellationToken.Register(() => _browserConnected.SetCanceled()); + cancellationToken.Register(() => _browserConnected.TrySetCanceled()); try { @@ -237,8 +237,12 @@ public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken) } public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) - => SendAsync(s_waitMessage, cancellationToken); + { + logger.Log(LogEvents.SendingWaitMessage); + return SendAsync(s_waitMessage, cancellationToken); + } + // obsolete: to be removed public ValueTask SendPingMessageAsync(CancellationToken cancellationToken) => SendAsync(s_pingMessage, cancellationToken); diff --git a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs index 507595eb89d1..498c26110089 100644 --- a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs +++ b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs @@ -37,7 +37,7 @@ public BrowserConnection(WebSocket clientSocket, string? sharedSecret, ILoggerFa ServerLogger = loggerFactory.CreateLogger(ServerLogComponentName, displayName); AgentLogger = loggerFactory.CreateLogger(AgentLogComponentName, displayName); - ServerLogger.LogDebug("Connected to referesh server."); + ServerLogger.Log(LogEvents.ConnectedToRefreshServer); } public void Dispose() diff --git a/src/BuiltInTools/dotnet-format/Formatters/CharsetFormatter.cs b/src/BuiltInTools/dotnet-format/Formatters/CharsetFormatter.cs index b2aa5af51847..5e8d9ee6cfad 100644 --- a/src/BuiltInTools/dotnet-format/Formatters/CharsetFormatter.cs +++ b/src/BuiltInTools/dotnet-format/Formatters/CharsetFormatter.cs @@ -68,7 +68,8 @@ private static byte[] GetEncodedBytes(string text, Encoding encoding) private static bool TryGetCharset(AnalyzerConfigOptions analyzerConfigOptions, [NotNullWhen(true)] out Encoding? encoding) { if (analyzerConfigOptions != null && - analyzerConfigOptions.TryGetValue("charset", out var charsetOption)) + analyzerConfigOptions.TryGetValue("charset", out var charsetOption) && + charsetOption != "unset") { encoding = GetCharset(charsetOption); return true; diff --git a/src/BuiltInTools/dotnet-watch.slnf b/src/BuiltInTools/dotnet-watch.slnf index 09d529c61e9c..b7454eb8869a 100644 --- a/src/BuiltInTools/dotnet-watch.slnf +++ b/src/BuiltInTools/dotnet-watch.slnf @@ -18,10 +18,11 @@ "src\\BuiltInTools\\HotReloadClient\\Microsoft.DotNet.HotReload.Client.shproj", "src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj", "test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", + "test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj", "test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", "test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj", - "test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj", + "test\\dotnet-watch-test-browser\\dotnet-watch-test-browser.csproj", "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj" ] } diff --git a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs index 1e84fb05a408..b7d669a89216 100644 --- a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs +++ b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs @@ -43,6 +43,7 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r private readonly Dictionary _sessions = []; private int _sessionIdDispenser; + private volatile bool _isDisposed; public SessionManager(ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions) @@ -82,10 +83,7 @@ public async ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancell _sessions.Clear(); } - foreach (var session in sessions) - { - await TerminateSessionAsync(session, cancellationToken); - } + await Task.WhenAll(sessions.Select(TerminateSessionAsync)).WaitAsync(cancellationToken); } public IEnumerable<(string name, string value)> GetEnvironmentVariables() @@ -113,7 +111,9 @@ public async ValueTask StartProjectAsync(string dcpId, string se var processTerminationSource = new CancellationTokenSource(); var outputChannel = Channel.CreateUnbounded(s_outputChannelOptions); - var runningProject = await _projectLauncher.TryLaunchProcessAsync( + RunningProject? runningProject = null; + + runningProject = await _projectLauncher.TryLaunchProcessAsync( projectOptions, processTerminationSource, onOutput: line => @@ -121,6 +121,21 @@ public async ValueTask StartProjectAsync(string dcpId, string se var writeResult = outputChannel.Writer.TryWrite(line); Debug.Assert(writeResult); }, + onExit: async (processId, exitCode) => + { + // Project can be null if the process exists while it's being initialized. + if (runningProject?.IsRestarting == false) + { + try + { + await _service.NotifySessionEndedAsync(dcpId, sessionId, processId, exitCode, cancellationToken); + } + catch (OperationCanceledException) + { + // canceled on shutdown, ignore + } + } + }, restartOperation: cancellationToken => StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken), cancellationToken); @@ -134,7 +149,7 @@ public async ValueTask StartProjectAsync(string dcpId, string se await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken); // cancel reading output when the process terminates: - var outputReader = StartChannelReader(processTerminationSource.Token); + var outputReader = StartChannelReader(runningProject.ProcessExitedCancellationToken); lock (_guard) { @@ -159,7 +174,7 @@ async Task StartChannelReader(CancellationToken cancellationToken) } catch (Exception e) { - if (e is not OperationCanceledException) + if (!cancellationToken.IsCancellationRequested) { _logger.LogError("Unexpected error reading output of session '{SessionId}': {Exception}", sessionId, e); } @@ -185,18 +200,15 @@ async ValueTask IAspireServerEvents.StopSessionAsync(string dcpId, string _sessions.Remove(sessionId); } - await TerminateSessionAsync(session, cancellationToken); + await TerminateSessionAsync(session); return true; } - private async ValueTask TerminateSessionAsync(Session session, CancellationToken cancellationToken) + private async Task TerminateSessionAsync(Session session) { _logger.LogDebug("Stop session #{SessionId}", session.Id); - var exitCode = await _projectLauncher.TerminateProcessAsync(session.RunningProject, cancellationToken); - - // Wait until the started notification has been sent so that we don't send out of order notifications: - await _service.NotifySessionEndedAsync(session.DcpId, session.Id, session.RunningProject.ProcessId, exitCode, cancellationToken); + await session.RunningProject.TerminateAsync(); // process termination should cancel output reader task: await session.OutputReader; diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs index 1c345803c215..c841191e0208 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs @@ -4,13 +4,14 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Microsoft.Build.Graph; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; -internal sealed class BrowserLauncher(ILogger logger, EnvironmentOptions environmentOptions) +internal sealed class BrowserLauncher(ILogger logger, IProcessOutputReporter processOutputReporter, EnvironmentOptions environmentOptions) { // interlocked private ImmutableHashSet _browserLaunchAttempted = []; @@ -61,18 +62,13 @@ public static string GetLaunchUrl(string? profileLaunchUrl, string outputLaunchU private void LaunchBrowser(string launchUrl, AbstractBrowserRefreshServer? server) { - var fileName = launchUrl; + var (fileName, arg, useShellExecute) = environmentOptions.BrowserPath is { } browserPath + ? (browserPath, launchUrl, false) + : (launchUrl, null, true); - var args = string.Empty; - if (environmentOptions.BrowserPath is { } browserPath) - { - args = fileName; - fileName = browserPath; - } + logger.Log(MessageDescriptor.LaunchingBrowser, fileName, arg); - logger.LogDebug("Launching browser: {FileName} {Args}", fileName, args); - - if (environmentOptions.TestFlags != TestFlags.None) + if (environmentOptions.TestFlags != TestFlags.None && environmentOptions.BrowserPath == null) { if (environmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser)) { @@ -83,29 +79,23 @@ private void LaunchBrowser(string launchUrl, AbstractBrowserRefreshServer? serve return; } - var info = new ProcessStartInfo + // dotnet-watch, by default, relies on URL file association to launch browsers. On Windows and MacOS, this works fairly well + // where URLs are associated with the default browser. On Linux, this is a bit murky. + // From emperical observation, it's noted that failing to launch a browser results in either Process.Start returning a null-value + // or for the process to have immediately exited. + // We can use this to provide a helpful message. + var processSpec = new ProcessSpec() { - FileName = fileName, - Arguments = args, - UseShellExecute = true, + Executable = fileName, + Arguments = arg != null ? [arg] : [], + UseShellExecute = useShellExecute, + OnOutput = environmentOptions.TestFlags.HasFlag(TestFlags.RedirectBrowserOutput) ? processOutputReporter.ReportOutput : null, }; - try - { - using var browserProcess = Process.Start(info); - if (browserProcess is null or { HasExited: true }) - { - // dotnet-watch, by default, relies on URL file association to launch browsers. On Windows and MacOS, this works fairly well - // where URLs are associated with the default browser. On Linux, this is a bit murky. - // From emperical observation, it's noted that failing to launch a browser results in either Process.Start returning a null-value - // or for the process to have immediately exited. - // We can use this to provide a helpful message. - logger.LogInformation("Unable to launch the browser. Url '{Url}'.", launchUrl); - } - } - catch (Exception e) + using var browserProcess = ProcessRunner.TryStartProcess(processSpec, logger); + if (browserProcess is null or { HasExited: true }) { - logger.LogDebug("Failed to launch a browser: {Message}", e.Message); + logger.LogWarning("Unable to launch the browser. Url '{Url}'.", launchUrl); } } diff --git a/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs b/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs index ca8f616ebcce..56bcba3427e6 100644 --- a/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs +++ b/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs @@ -43,6 +43,10 @@ internal static class ProjectGraphUtilities } catch (Exception e) when (e is not OperationCanceledException) { + // ProejctGraph aggregates OperationCanceledException exception, + // throw here to propagate the cancellation. + cancellationToken.ThrowIfCancellationRequested(); + logger.LogDebug("Failed to load project graph."); if (e is AggregateException { InnerExceptions: var innerExceptions }) diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs index f2fbfa7cabd3..72f787332533 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs @@ -22,6 +22,11 @@ internal enum TestFlags /// This allows tests to trigger key based events. /// ReadKeyFromStdin = 1 << 3, + + /// + /// Redirects the output of the launched browser process to watch output. + /// + RedirectBrowserOutput = 1 << 4, } internal sealed record EnvironmentOptions( diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs index cedccb2351f1..c29aee037ca2 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs @@ -15,7 +15,6 @@ namespace Microsoft.DotNet.Watch internal sealed class CompilationHandler : IDisposable { public readonly IncrementalMSBuildWorkspace Workspace; - private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly WatchHotReloadService _hotReloadService; private readonly ProcessRunner _processRunner; @@ -40,9 +39,8 @@ internal sealed class CompilationHandler : IDisposable private bool _isDisposed; - public CompilationHandler(ILoggerFactory loggerFactory, ILogger logger, ProcessRunner processRunner) + public CompilationHandler(ILogger logger, ProcessRunner processRunner) { - _loggerFactory = loggerFactory; _logger = logger; _processRunner = processRunner; Workspace = new IncrementalMSBuildWorkspace(logger); @@ -57,15 +55,8 @@ public void Dispose() public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken) { - _logger.LogDebug("Disposing remaining child processes."); - - var projectsToDispose = await TerminateNonRootProcessesAsync(projectPaths: null, cancellationToken); - - foreach (var project in projectsToDispose) - { - project.Dispose(); - } - + _logger.LogDebug("Terminating remaining child processes."); + await TerminateNonRootProcessesAsync(projectPaths: null, cancellationToken); Dispose(); } @@ -101,19 +92,35 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) CancellationToken cancellationToken) { var processExitedSource = new CancellationTokenSource(); - var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(processExitedSource.Token, cancellationToken); + + // Cancel process communication as soon as process termination is requested, shutdown is requested, or the process exits (whichever comes first). + // If we only cancel after we process exit event handler is triggered the pipe might have already been closed and may fail unexpectedly. + using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(processTerminationSource.Token, processExitedSource.Token, cancellationToken); + var processCommunicationCancellationToken = processCommunicationCancellationSource.Token; // Dispose these objects on failure: - using var disposables = new Disposables([clients, processExitedSource, processCommunicationCancellationSource]); + using var disposables = new Disposables([clients, processExitedSource]); // It is important to first create the named pipe connection (Hot Reload client is the named pipe server) // and then start the process (named pipe client). Otherwise, the connection would fail. - clients.InitiateConnection(processCommunicationCancellationSource.Token); + clients.InitiateConnection(processCommunicationCancellationToken); + + RunningProject? publishedRunningProject = null; - processSpec.OnExit += (_, _) => + var previousOnExit = processSpec.OnExit; + processSpec.OnExit = async (processId, exitCode) => { - processExitedSource.Cancel(); - return ValueTask.CompletedTask; + // Await the previous action so that we only clean up after all requested "on exit" actions have been completed. + if (previousOnExit != null) + { + await previousOnExit(processId, exitCode); + } + + // Remove the running project if it has been published to _runningProjects (if it hasn't exited during initialization): + if (publishedRunningProject != null && RemoveRunningProject(publishedRunningProject)) + { + publishedRunningProject.Dispose(); + } }; var launchResult = new ProcessLaunchResult(); @@ -124,79 +131,129 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) return null; } - // Wait for agent to create the name pipe and send capabilities over. - // the agent blocks the app execution until initial updates are applied (if any). - var capabilities = await clients.GetUpdateCapabilitiesAsync(processCommunicationCancellationSource.Token); - - var runningProject = new RunningProject( - projectNode, - projectOptions, - clients, - runningProcess, - launchResult.ProcessId.Value, - processExitedSource: processExitedSource, - processTerminationSource: processTerminationSource, - restartOperation: restartOperation, - disposables: [processCommunicationCancellationSource], - capabilities); - var projectPath = projectNode.ProjectInstance.FullPath; - // ownership transferred to running project: - disposables.Items.Clear(); - disposables.Items.Add(runningProject); - - var appliedUpdateCount = 0; - while (true) + try { - // Observe updates that need to be applied to the new process - // and apply them before adding it to running processes. - // Do not block on udpates being made to other processes to avoid delaying the new process being up-to-date. - var updatesToApply = _previousUpdates.Skip(appliedUpdateCount).ToImmutableArray(); - if (updatesToApply.Any()) + // Wait for agent to create the name pipe and send capabilities over. + // the agent blocks the app execution until initial updates are applied (if any). + var capabilities = await clients.GetUpdateCapabilitiesAsync(processCommunicationCancellationToken); + + var runningProject = new RunningProject( + projectNode, + projectOptions, + clients, + runningProcess, + launchResult.ProcessId.Value, + processExitedSource: processExitedSource, + processTerminationSource: processTerminationSource, + restartOperation: restartOperation, + capabilities); + + // ownership transferred to running project: + disposables.Items.Clear(); + disposables.Items.Add(runningProject); + + var appliedUpdateCount = 0; + while (true) { - await clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updatesToApply), isProcessSuspended: false, processCommunicationCancellationSource.Token); - } - - appliedUpdateCount += updatesToApply.Length; - - lock (_runningProjectsAndUpdatesGuard) - { - ObjectDisposedException.ThrowIf(_isDisposed, this); - - // More updates might have come in while we have been applying updates. - // If so, continue updating. - if (_previousUpdates.Count > appliedUpdateCount) + // Observe updates that need to be applied to the new process + // and apply them before adding it to running processes. + // Do not block on udpates being made to other processes to avoid delaying the new process being up-to-date. + var updatesToApply = _previousUpdates.Skip(appliedUpdateCount).ToImmutableArray(); + if (updatesToApply.Any()) { - continue; + await clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updatesToApply), isProcessSuspended: false, isInitial: true, processCommunicationCancellationToken); } - // Only add the running process after it has been up-to-date. - // This will prevent new updates being applied before we have applied all the previous updates. - if (!_runningProjects.TryGetValue(projectPath, out var projectInstances)) + appliedUpdateCount += updatesToApply.Length; + + lock (_runningProjectsAndUpdatesGuard) { - projectInstances = []; + ObjectDisposedException.ThrowIf(_isDisposed, this); + + // More updates might have come in while we have been applying updates. + // If so, continue updating. + if (_previousUpdates.Count > appliedUpdateCount) + { + continue; + } + + // Only add the running process after it has been up-to-date. + // This will prevent new updates being applied before we have applied all the previous updates. + if (!_runningProjects.TryGetValue(projectPath, out var projectInstances)) + { + projectInstances = []; + } + + _runningProjects = _runningProjects.SetItem(projectPath, projectInstances.Add(runningProject)); + + // ownership transferred to _runningProjects + publishedRunningProject = runningProject; + disposables.Items.Clear(); + break; } + } - _runningProjects = _runningProjects.SetItem(projectPath, projectInstances.Add(runningProject)); + clients.OnRuntimeRudeEdit += (code, message) => + { + // fire and forget: + _ = HandleRuntimeRudeEditAsync(runningProject, message, cancellationToken); + }; - // ownership transferred to _runningProjects - disposables.Items.Clear(); - break; + // Notifies the agent that it can unblock the execution of the process: + await clients.InitialUpdatesAppliedAsync(processCommunicationCancellationToken); + + // If non-empty solution is loaded into the workspace (a Hot Reload session is active): + if (Workspace.CurrentSolution is { ProjectIds: not [] } currentSolution) + { + // Preparing the compilation is a perf optimization. We can skip it if the session hasn't been started yet. + PrepareCompilations(currentSolution, projectPath, cancellationToken); } + + return runningProject; } + catch (OperationCanceledException) when (processExitedSource.IsCancellationRequested) + { + // Process exited during initialization. This should not happen since we control the process during this time. + _logger.LogError("Failed to launch '{ProjectPath}'. Process {PID} exited during initialization.", projectPath, launchResult.ProcessId); + return null; + } + } - // Notifies the agent that it can unblock the execution of the process: - await clients.InitialUpdatesAppliedAsync(cancellationToken); + private async Task HandleRuntimeRudeEditAsync(RunningProject runningProject, string rudeEditMessage, CancellationToken cancellationToken) + { + var logger = runningProject.Clients.ClientLogger; - // If non-empty solution is loaded into the workspace (a Hot Reload session is active): - if (Workspace.CurrentSolution is { ProjectIds: not [] } currentSolution) + try { - // Preparing the compilation is a perf optimization. We can skip it if the session hasn't been started yet. - PrepareCompilations(currentSolution, projectPath, cancellationToken); - } + // Always auto-restart on runtime rude edits regardless of the settings. + // Since there is no debugger attached the process would crash on an unhandled HotReloadException if + // we let it continue executing. + logger.LogWarning(rudeEditMessage); + logger.Log(MessageDescriptor.RestartingApplication); + + if (!runningProject.InitiateRestart()) + { + // Already in the process of restarting, possibly because of another runtime rude edit. + return; + } - return runningProject; + await runningProject.Clients.ReportCompilationErrorsInApplicationAsync([rudeEditMessage, MessageDescriptor.RestartingApplication.GetMessage()], cancellationToken); + + // Terminate the process. + await runningProject.TerminateAsync(); + + // Creates a new running project and launches it: + await runningProject.RestartOperation(cancellationToken); + } + catch (Exception e) + { + if (e is not OperationCanceledException) + { + logger.LogError("Failed to handle runtime rude edit: {Exception}", e.ToString()); + } + } } private ImmutableArray GetAggregateCapabilities() @@ -229,7 +286,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C ImmutableArray projectUpdates, ImmutableArray projectsToRebuild, ImmutableArray projectsToRedeploy, - ImmutableArray terminatedProjects)> HandleManagedCodeChangesAsync( + ImmutableArray projectsToRestart)> HandleManagedCodeChangesAsync( bool autoRestart, Func, CancellationToken, Task> restartPrompt, CancellationToken cancellationToken) @@ -284,11 +341,11 @@ private static void PrepareCompilations(Solution solution, string projectPath, C // Terminate all tracked processes that need to be restarted, // except for the root process, which will terminate later on. - var terminatedProjects = updates.ProjectsToRestart.IsEmpty + var projectsToRestart = updates.ProjectsToRestart.IsEmpty ? [] : await TerminateNonRootProcessesAsync(updates.ProjectsToRestart.Select(e => currentSolution.GetProject(e.Key)!.FilePath!), cancellationToken); - return (updates.ProjectUpdates, projectsToRebuild, projectsToRedeploy, terminatedProjects); + return (updates.ProjectUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart); } public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) @@ -312,10 +369,10 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT { try { - using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedSource.Token, cancellationToken); - await runningProject.Clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updates), isProcessSuspended: false, processCommunicationCancellationSource.Token); + using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); + await runningProject.Clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updates), isProcessSuspended: false, isInitial: false, processCommunicationCancellationSource.Token); } - catch (OperationCanceledException) when (runningProject.ProcessExitedSource.Token.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (runningProject.ProcessExitedCancellationToken.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { runningProject.Clients.ClientLogger.Log(MessageDescriptor.HotReloadCanceledProcessExited); } @@ -363,7 +420,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updat _logger.Log(MessageDescriptor.RestartNeededToApplyChanges); } - var diagnosticsToDisplayInApp = new List(); + var errorsToDisplayInApp = new List(); // Display errors first, then warnings: ReportCompilationDiagnostics(DiagnosticSeverity.Error); @@ -373,7 +430,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updat // report or clear diagnostics in the browser UI await ForEachProjectAsync( _runningProjects, - (project, cancellationToken) => project.Clients.ReportCompilationErrorsInApplicationAsync([.. diagnosticsToDisplayInApp], cancellationToken).AsTask() ?? Task.CompletedTask, + (project, cancellationToken) => project.Clients.ReportCompilationErrorsInApplicationAsync([.. errorsToDisplayInApp], cancellationToken).AsTask() ?? Task.CompletedTask, cancellationToken); void ReportCompilationDiagnostics(DiagnosticSeverity severity) @@ -437,16 +494,20 @@ void ReportRudeEdits() bool IsAutoRestartEnabled(ProjectId id) => runningProjectInfos.TryGetValue(id, out var info) && info.RestartWhenChangesHaveNoEffect; - void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, string prefix = "") + void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, string autoPrefix = "") { var display = CSharpDiagnosticFormatter.Instance.Format(diagnostic); - var args = new[] { prefix, display }; + var args = new[] { autoPrefix, display }; _logger.Log(descriptor, args); - if (descriptor.Severity != MessageSeverity.None) + if (autoPrefix != "") + { + errorsToDisplayInApp.Add(MessageDescriptor.RestartingApplicationToApplyChanges.GetMessage()); + } + else if (descriptor.Severity != MessageSeverity.None) { - diagnosticsToDisplayInApp.Add(descriptor.GetMessage(args)); + errorsToDisplayInApp.Add(descriptor.GetMessage(args)); } } @@ -528,7 +589,8 @@ public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList { var (runningProject, assets) = entry; - await runningProject.Clients.ApplyStaticAssetUpdatesAsync(assets, cancellationToken); + using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); + await runningProject.Clients.ApplyStaticAssetUpdatesAsync(assets, processCommunicationCancellationSource.Token); }); await Task.WhenAll(tasks).WaitAsync(cancellationToken); @@ -539,76 +601,64 @@ public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList - /// Terminates all processes launched for projects with , + /// Terminates all processes launched for non-root projects with , /// or all running non-root project processes if is null. /// /// Removes corresponding entries from . /// /// Does not terminate the root project. /// + /// All processes (including root) to be restarted. internal async ValueTask> TerminateNonRootProcessesAsync( IEnumerable? projectPaths, CancellationToken cancellationToken) { ImmutableArray projectsToRestart = []; - UpdateRunningProjects(runningProjectsByPath => + lock (_runningProjectsAndUpdatesGuard) { - if (projectPaths == null) - { - projectsToRestart = _runningProjects.SelectMany(entry => entry.Value).Where(p => !p.Options.IsRootProject).ToImmutableArray(); - return _runningProjects.Clear(); - } - - projectsToRestart = projectPaths.SelectMany(path => _runningProjects.TryGetValue(path, out var array) ? array : []).ToImmutableArray(); - return runningProjectsByPath.RemoveRange(projectPaths); - }); + projectsToRestart = projectPaths == null + ? [.. _runningProjects.SelectMany(entry => entry.Value)] + : [.. projectPaths.SelectMany(path => _runningProjects.TryGetValue(path, out var array) ? array : [])]; + } // Do not terminate root process at this time - it would signal the cancellation token we are currently using. // The process will be restarted later on. - var projectsToTerminate = projectsToRestart.Where(p => !p.Options.IsRootProject); - - // wait for all processes to exit to release their resources, so we can rebuild: - _ = await TerminateRunningProjects(projectsToTerminate, cancellationToken); + // Wait for all processes to exit to release their resources, so we can rebuild. + await Task.WhenAll(projectsToRestart.Where(p => !p.Options.IsRootProject).Select(p => p.TerminateForRestartAsync())).WaitAsync(cancellationToken); return projectsToRestart; } - /// - /// Terminates process of the given . - /// Removes corresponding entries from . - /// - /// Should not be called with the root project. - /// - /// Exit code of the terminated process. - internal async ValueTask TerminateNonRootProcessAsync(RunningProject project, CancellationToken cancellationToken) + private bool RemoveRunningProject(RunningProject project) { - Debug.Assert(!project.Options.IsRootProject); - var projectPath = project.ProjectNode.ProjectInstance.FullPath; - UpdateRunningProjects(runningProjectsByPath => + return UpdateRunningProjects(runningProjectsByPath => { - if (!runningProjectsByPath.TryGetValue(projectPath, out var runningProjects) || - runningProjects.Remove(project) is var updatedRunningProjects && runningProjects == updatedRunningProjects) + if (!runningProjectsByPath.TryGetValue(projectPath, out var runningInstances)) { - _logger.LogDebug("Ignoring an attempt to terminate process {ProcessId} of project '{Path}' that has no associated running processes.", project.ProcessId, projectPath); return runningProjectsByPath; } + var updatedRunningProjects = runningInstances.Remove(project); return updatedRunningProjects is [] ? runningProjectsByPath.Remove(projectPath) : runningProjectsByPath.SetItem(projectPath, updatedRunningProjects); }); - - // wait for all processes to exit to release their resources: - return (await TerminateRunningProjects([project], cancellationToken)).Single(); } - private void UpdateRunningProjects(Func>, ImmutableDictionary>> updater) + private bool UpdateRunningProjects(Func>, ImmutableDictionary>> updater) { lock (_runningProjectsAndUpdatesGuard) { - _runningProjects = updater(_runningProjects); + var newRunningProjects = updater(_runningProjects); + if (newRunningProjects != _runningProjects) + { + _runningProjects = newRunningProjects; + return true; + } + + return false; } } @@ -620,12 +670,6 @@ public bool TryGetRunningProject(string projectPath, out ImmutableArray> TerminateRunningProjects(IEnumerable projects, CancellationToken cancellationToken) - { - // wait for all tasks to complete: - return await Task.WhenAll(projects.Select(p => p.TerminateAsync().AsTask())).WaitAsync(cancellationToken); - } - private static Task ForEachProjectAsync(ImmutableDictionary> projects, Func action, CancellationToken cancellationToken) => Task.WhenAll(projects.SelectMany(entry => entry.Value).Select(project => action(project, cancellationToken))).WaitAsync(cancellationToken); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs index 764a371ab309..38e313f6f746 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs @@ -108,7 +108,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) } var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, _context.Logger); - compilationHandler = new CompilationHandler(_context.LoggerFactory, _context.Logger, _context.ProcessRunner); + compilationHandler = new CompilationHandler(_context.Logger, _context.ProcessRunner); var scopedCssFileHandler = new ScopedCssFileHandler(_context.Logger, _context.BuildLogger, projectMap, _context.BrowserRefreshServerFactory, _context.Options, _context.EnvironmentOptions); var projectLauncher = new ProjectLauncher(_context, projectMap, compilationHandler, iteration); evaluationResult.ItemExclusions.Report(_context.Logger); @@ -127,7 +127,8 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) rootProjectOptions, rootProcessTerminationSource, onOutput: null, - restartOperation: new RestartOperation(_ => throw new InvalidOperationException("Root project shouldn't be restarted")), + onExit: null, + restartOperation: new RestartOperation(_ => default), // the process will automatically restart iterationCancellationToken); if (rootRunningProject == null) @@ -138,7 +139,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) } // Cancel iteration as soon as the root process exits, so that we don't spent time loading solution, etc. when the process is already dead. - rootRunningProject.ProcessExitedSource.Token.Register(() => iterationCancellationSource.Cancel()); + rootRunningProject.ProcessExitedCancellationToken.Register(() => iterationCancellationSource.Cancel()); if (shutdownCancellationToken.IsCancellationRequested) { @@ -146,11 +147,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) return; } - try - { - await rootRunningProject.WaitForProcessRunningAsync(iterationCancellationToken); - } - catch (OperationCanceledException) when (rootRunningProject.ProcessExitedSource.Token.IsCancellationRequested) + if (!await rootRunningProject.WaitForProcessRunningAsync(iterationCancellationToken)) { // Process might have exited while we were trying to communicate with it. // Cancel the iteration, but wait for a file change before starting a new one. @@ -384,19 +381,7 @@ await Task.WhenAll( projectsToRestart.Select(async runningProject => { var newRunningProject = await runningProject.RestartOperation(shutdownCancellationToken); - - try - { - await newRunningProject.WaitForProcessRunningAsync(shutdownCancellationToken); - } - catch (OperationCanceledException) when (!shutdownCancellationToken.IsCancellationRequested) - { - // Process might have exited while we were trying to communicate with it. - } - finally - { - runningProject.Dispose(); - } + _ = await newRunningProject.WaitForProcessRunningAsync(shutdownCancellationToken); })) .WaitAsync(shutdownCancellationToken); @@ -554,11 +539,10 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra await runtimeProcessLauncher.DisposeAsync(); } - rootRunningProject?.Dispose(); - if (waitForFileChangeBeforeRestarting && !shutdownCancellationToken.IsCancellationRequested && - !forceRestartCancellationSource.IsCancellationRequested) + !forceRestartCancellationSource.IsCancellationRequested && + rootRunningProject?.IsRestarting != true) { using var shutdownOrForcedRestartSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, forceRestartCancellationSource.Token); await WaitForFileChangeBeforeRestarting(fileWatcher, evaluationResult, shutdownOrForcedRestartSource.Token); diff --git a/src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs b/src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs index 3583dc07b8d8..54ae7d130229 100644 --- a/src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs +++ b/src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs @@ -9,12 +9,22 @@ namespace Microsoft.DotNet.Watch { internal sealed class ProcessRunner(TimeSpan processCleanupTimeout) { - private sealed class ProcessState + private sealed class ProcessState(Process process) : IDisposable { + public Process Process { get; } = process; + public int ProcessId; public bool HasExited; + + // True if Ctrl+C was sent to the process on Windows. + public bool SentWindowsCtrlC; + + public void Dispose() + => Process.Dispose(); } + private const int CtlrCExitCode = unchecked((int)0xC000013A); + // For testing purposes only, lock on access. private static readonly HashSet s_runningApplicationProcesses = []; @@ -31,67 +41,32 @@ public static IReadOnlyCollection GetRunningApplicationProcesses() /// public async Task RunAsync(ProcessSpec processSpec, ILogger logger, ProcessLaunchResult? launchResult, CancellationToken processTerminationToken) { - var state = new ProcessState(); var stopwatch = new Stopwatch(); - - var onOutput = processSpec.OnOutput; - - using var process = CreateProcess(processSpec, onOutput, state, logger); - stopwatch.Start(); - Exception? launchException = null; - try - { - if (!process.Start()) - { - throw new InvalidOperationException("Process can't be started."); - } - - state.ProcessId = process.Id; - - if (processSpec.IsUserApplication) - { - lock (s_runningApplicationProcesses) - { - s_runningApplicationProcesses.Add(state.ProcessId); - } - } - - if (onOutput != null) - { - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - } - } - catch (Exception e) - { - launchException = e; - } - - var argsDisplay = processSpec.GetArgumentsDisplay(); - if (launchException == null) + using var state = TryStartProcessImpl(processSpec, logger); + if (state == null) { - logger.Log(MessageDescriptor.LaunchedProcess, processSpec.Executable, argsDisplay, state.ProcessId); - } - else - { - logger.Log(MessageDescriptor.FailedToLaunchProcess, processSpec.Executable, argsDisplay, launchException.Message); return int.MinValue; } - if (launchResult != null) + if (processSpec.IsUserApplication) { - launchResult.ProcessId = process.Id; + lock (s_runningApplicationProcesses) + { + s_runningApplicationProcesses.Add(state.ProcessId); + } } + launchResult?.ProcessId = state.ProcessId; + int? exitCode = null; try { try { - await process.WaitForExitAsync(processTerminationToken); + await state.Process.WaitForExitAsync(processTerminationToken); } catch (OperationCanceledException) { @@ -99,7 +74,7 @@ public async Task RunAsync(ProcessSpec processSpec, ILogger logger, Process // Either Ctrl+C was pressed or the process is being restarted. // Non-cancellable to not leave orphaned processes around blocking resources: - await TerminateProcessAsync(process, processSpec, state, logger, CancellationToken.None); + await TerminateProcessAsync(state.Process, processSpec, state, logger, CancellationToken.None); } } catch (Exception e) @@ -125,18 +100,18 @@ public async Task RunAsync(ProcessSpec processSpec, ILogger logger, Process try { - exitCode = process.ExitCode; + exitCode = state.Process.ExitCode; } catch { exitCode = null; } - logger.Log(MessageDescriptor.ProcessRunAndExited, process.Id, stopwatch.ElapsedMilliseconds, exitCode); + logger.Log(MessageDescriptor.ProcessRunAndExited, state.ProcessId, stopwatch.ElapsedMilliseconds, exitCode); if (processSpec.IsUserApplication) { - if (exitCode == 0) + if (exitCode == 0 || state.SentWindowsCtrlC && exitCode == CtlrCExitCode) { logger.Log(MessageDescriptor.Exited); } @@ -159,21 +134,28 @@ public async Task RunAsync(ProcessSpec processSpec, ILogger logger, Process return exitCode ?? int.MinValue; } - private static Process CreateProcess(ProcessSpec processSpec, Action? onOutput, ProcessState state, ILogger logger) + internal static Process? TryStartProcess(ProcessSpec processSpec, ILogger logger) + => TryStartProcessImpl(processSpec, logger)?.Process; + + private static ProcessState? TryStartProcessImpl(ProcessSpec processSpec, ILogger logger) { + var onOutput = processSpec.OnOutput; + var process = new Process { EnableRaisingEvents = true, StartInfo = { FileName = processSpec.Executable, - UseShellExecute = false, + UseShellExecute = processSpec.UseShellExecute, WorkingDirectory = processSpec.WorkingDirectory, RedirectStandardOutput = onOutput != null, RedirectStandardError = onOutput != null, } }; + var state = new ProcessState(process); + if (processSpec.IsUserApplication && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { process.StartInfo.CreateNewProcessGroup = true; @@ -229,14 +211,38 @@ private static Process CreateProcess(ProcessSpec processSpec, Action }; } - return process; + var argsDisplay = processSpec.GetArgumentsDisplay(); + + try + { + if (!process.Start()) + { + throw new InvalidOperationException("Process can't be started."); + } + state.ProcessId = process.Id; + + if (onOutput != null) + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + } + + logger.Log(MessageDescriptor.LaunchedProcess, processSpec.Executable, argsDisplay, state.ProcessId); + return state; + } + catch (Exception e) + { + logger.Log(MessageDescriptor.FailedToLaunchProcess, processSpec.Executable, argsDisplay, e.Message); + + state.Dispose(); + return null; + } } private async ValueTask TerminateProcessAsync(Process process, ProcessSpec processSpec, ProcessState state, ILogger logger, CancellationToken cancellationToken) { var forceOnly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !processSpec.IsUserApplication; - // Ctrl+C hasn't been sent. TerminateProcess(process, state, logger, forceOnly); if (forceOnly) @@ -356,7 +362,11 @@ private static void TerminateWindowsProcess(Process process, ProcessState state, else { var error = ProcessUtilities.SendWindowsCtrlCEvent(state.ProcessId); - if (error != null) + if (error == null) + { + state.SentWindowsCtrlC = true; + } + else { logger.Log(MessageDescriptor.FailedToSendSignalToProcess, signalName, state.ProcessId, error); } diff --git a/src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs b/src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs index b780f2770029..b3e1eaa1a6a6 100644 --- a/src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs +++ b/src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs @@ -8,12 +8,13 @@ internal sealed class ProcessSpec { public string? Executable { get; set; } public string? WorkingDirectory { get; set; } - public Dictionary EnvironmentVariables { get; } = new(); + public Dictionary EnvironmentVariables { get; } = []; public IReadOnlyList? Arguments { get; set; } public string? EscapedArguments { get; set; } public Action? OnOutput { get; set; } public ProcessExitAction? OnExit { get; set; } public CancellationToken CancelOutputCapture { get; set; } + public bool UseShellExecute { get; set; } = false; /// /// True if the process is a user application, false if it is a helper process (e.g. dotnet build). diff --git a/src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs b/src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs index 3299710fd18f..fb74333ebdd3 100644 --- a/src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs +++ b/src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs @@ -30,6 +30,7 @@ public EnvironmentOptions EnvironmentOptions ProjectOptions projectOptions, CancellationTokenSource processTerminationSource, Action? onOutput, + ProcessExitAction? onExit, RestartOperation restartOperation, CancellationToken cancellationToken) { @@ -66,12 +67,14 @@ public EnvironmentOptions EnvironmentOptions IsUserApplication = true, WorkingDirectory = projectOptions.WorkingDirectory, OnOutput = onOutput, + OnExit = onExit, }; // Stream output lines to the process output reporter. // The reporter synchronizes the output of the process with the logger output, // so that the printed lines don't interleave. - processSpec.OnOutput += line => + // Only send the output to the reporter if no custom output handler was provided (e.g. for Aspire child processes). + processSpec.OnOutput ??= line => { context.ProcessOutputReporter.ReportOutput(context.ProcessOutputReporter.PrefixProcessOutput ? line with { Content = $"[{projectDisplayName}] {line.Content}" } : line); }; @@ -88,7 +91,7 @@ public EnvironmentOptions EnvironmentOptions environmentBuilder[EnvironmentVariables.Names.DotnetWatch] = "1"; environmentBuilder[EnvironmentVariables.Names.DotnetWatchIteration] = (Iteration + 1).ToString(CultureInfo.InvariantCulture); - if (Logger.IsEnabled(LogLevel.Debug)) + if (Logger.IsEnabled(LogLevel.Trace)) { environmentBuilder[EnvironmentVariables.Names.HotReloadDeltaClientLogMessages] = (EnvironmentOptions.SuppressEmojis ? Emoji.Default : Emoji.Agent).GetLogMessagePrefix() + $"[{projectDisplayName}]"; @@ -129,7 +132,4 @@ private static IReadOnlyList GetProcessArguments(ProjectOptions projectO arguments.AddRange(projectOptions.CommandArguments); return arguments; } - - public ValueTask TerminateProcessAsync(RunningProject project, CancellationToken cancellationToken) - => compilationHandler.TerminateNonRootProcessAsync(project, cancellationToken); } diff --git a/src/BuiltInTools/dotnet-watch/Process/RunningProject.cs b/src/BuiltInTools/dotnet-watch/Process/RunningProject.cs index 46ecd2c629a1..8f5bfe94b004 100644 --- a/src/BuiltInTools/dotnet-watch/Process/RunningProject.cs +++ b/src/BuiltInTools/dotnet-watch/Process/RunningProject.cs @@ -3,8 +3,10 @@ using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.Build.Graph; using Microsoft.DotNet.HotReload; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch { @@ -19,7 +21,6 @@ internal sealed class RunningProject( CancellationTokenSource processExitedSource, CancellationTokenSource processTerminationSource, RestartOperation restartOperation, - IReadOnlyList disposables, ImmutableArray capabilities) : IDisposable { public readonly ProjectGraphNode ProjectNode = projectNode; @@ -31,45 +32,84 @@ internal sealed class RunningProject( public readonly RestartOperation RestartOperation = restartOperation; /// - /// Cancellation source triggered when the process exits. + /// Cancellation token triggered when the process exits. + /// Stores the token to allow callers to use the token even after the source has been disposed. /// - public readonly CancellationTokenSource ProcessExitedSource = processExitedSource; + public CancellationToken ProcessExitedCancellationToken = processExitedSource.Token; /// - /// Cancellation source to use to terminate the process. + /// Set to true when the process termination is being requested so that it can be restarted within + /// the Hot Reload session (i.e. without restarting the root project). /// - public readonly CancellationTokenSource ProcessTerminationSource = processTerminationSource; + public bool IsRestarting => _isRestarting != 0; + + private volatile int _isRestarting; + private volatile bool _isDisposed; /// - /// Misc disposable object to dispose when the object is disposed. + /// Disposes the project. Can occur unexpectedly whenever the process exits. + /// Must only be called once per project. /// - private readonly IReadOnlyList _disposables = disposables; - public void Dispose() { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + _isDisposed = true; + processExitedSource.Cancel(); + Clients.Dispose(); - ProcessTerminationSource.Dispose(); - ProcessExitedSource.Dispose(); + processTerminationSource.Dispose(); + processExitedSource.Dispose(); + } + + /// + /// Waits for the application process to start. + /// Ensures that the build has been complete and the build outputs are available. + /// Returns false if the process has exited before the connection was established. + /// + public async ValueTask WaitForProcessRunningAsync(CancellationToken cancellationToken) + { + using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ProcessExitedCancellationToken); - foreach (var disposable in _disposables) + try { - disposable.Dispose(); + await Clients.WaitForConnectionEstablishedAsync(processCommunicationCancellationSource.Token); + return true; + } + catch (OperationCanceledException) when (ProcessExitedCancellationToken.IsCancellationRequested) + { + return false; } } /// - /// Waits for the application process to start. - /// Ensures that the build has been complete and the build outputs are available. + /// Terminates the process if it hasn't terminated yet. /// - public async ValueTask WaitForProcessRunningAsync(CancellationToken cancellationToken) + public Task TerminateAsync() { - await Clients.WaitForConnectionEstablishedAsync(cancellationToken); + if (!_isDisposed) + { + processTerminationSource.Cancel(); + } + + return RunningProcess; } - public async ValueTask TerminateAsync() + /// + /// Marks the as restarting. + /// Subsequent process termination will be treated as a restart. + /// + /// True if the project hasn't been int restarting state prior the call. + public bool InitiateRestart() + => Interlocked.Exchange(ref _isRestarting, 1) == 0; + + /// + /// Terminates the process in preparation for a restart. + /// + public Task TerminateForRestartAsync() { - ProcessTerminationSource.Cancel(); - return await RunningProcess; + InitiateRestart(); + return TerminateAsync(); } } } diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index b2b9bd03bbd9..f67a360a3a32 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -240,7 +240,7 @@ internal DotNetWatchContext CreateContext(ProcessRunner processRunner) EnvironmentOptions = environmentOptions, RootProjectOptions = rootProjectOptions, BrowserRefreshServerFactory = new BrowserRefreshServerFactory(), - BrowserLauncher = new BrowserLauncher(logger, environmentOptions), + BrowserLauncher = new BrowserLauncher(logger, processOutputReporter, environmentOptions), }; } diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json index b6981fd0ae9d..9e28729eb807 100644 --- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json +++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json @@ -3,7 +3,7 @@ "dotnet-watch": { "commandName": "Project", "commandLineArgs": "--verbose -bl", - "workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm", + "workingDirectory": "C:\\bugs\\9756\\aspire-watch-start-issue\\Aspire.AppHost", "environmentVariables": { "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)", "DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000", diff --git a/src/BuiltInTools/dotnet-watch/UI/IReporter.cs b/src/BuiltInTools/dotnet-watch/UI/IReporter.cs index c6f64394da88..319fcc1740b0 100644 --- a/src/BuiltInTools/dotnet-watch/UI/IReporter.cs +++ b/src/BuiltInTools/dotnet-watch/UI/IReporter.cs @@ -209,9 +209,13 @@ public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool conditi public static readonly MessageDescriptor UpdatingDiagnostics = Create(LogEvents.UpdatingDiagnostics, Emoji.Default); public static readonly MessageDescriptor FailedToReceiveResponseFromConnectedBrowser = Create(LogEvents.FailedToReceiveResponseFromConnectedBrowser, Emoji.Default); public static readonly MessageDescriptor NoBrowserConnected = Create(LogEvents.NoBrowserConnected, Emoji.Default); + public static readonly MessageDescriptor LaunchingBrowser = Create("Launching browser: {0} {1}", Emoji.Default, MessageSeverity.Verbose); public static readonly MessageDescriptor RefreshingBrowser = Create(LogEvents.RefreshingBrowser, Emoji.Default); public static readonly MessageDescriptor ReloadingBrowser = Create(LogEvents.ReloadingBrowser, Emoji.Default); public static readonly MessageDescriptor RefreshServerRunningAt = Create(LogEvents.RefreshServerRunningAt, Emoji.Default); + public static readonly MessageDescriptor ConnectedToRefreshServer = Create(LogEvents.ConnectedToRefreshServer, Emoji.Default); + public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, MessageSeverity.Output); + public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, MessageSeverity.Output); public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, MessageSeverity.Verbose); public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, MessageSeverity.Verbose); public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, MessageSeverity.Verbose); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs index 69b532afb0bb..0c5aef161367 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -35,6 +35,14 @@ public static class Constants public const string Identity = nameof(Identity); public const string FullPath = nameof(FullPath); + // MSBuild CLI flags + + /// + /// Disables the live-updating node display in the terminal logger, which is useful for LLM/agentic environments. + /// + public const string TerminalLogger_DisableNodeDisplay = "-tlp:DISABLENODEDISPLAY"; + + public static readonly string ProjectArgumentName = ""; public static readonly string SolutionArgumentName = ""; public static readonly string ToolPackageArgumentName = ""; diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs index 97e7abd086c3..e0dca709ac3c 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs @@ -56,7 +56,6 @@ public MSBuildForwardingAppWithoutLogging(MSBuildArgs msbuildArgs, string? msbui msbuildArgs.OtherMSBuildArgs.Add("-nologo"); } string? tlpDefault = TerminalLoggerDefault; - // new for .NET 9 - default TL to auto (aka enable in non-CI scenarios) if (string.IsNullOrWhiteSpace(tlpDefault)) { tlpDefault = "auto"; diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.Designer.cs b/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.Designer.cs index ecf9c58147f5..0b1407d3d11b 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.Designer.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.Designer.cs @@ -1439,6 +1439,17 @@ internal static string PostAction_ModifyJson_Error_NoJsonFile { } } + /// + /// Looks up a localized string similar to The result of parsing the following JSON was 'null': + /// + ///{0}. + /// + internal static string PostAction_ModifyJson_Error_NullJson { + get { + return ResourceManager.GetString("PostAction_ModifyJson_Error_NullJson", resourceCulture); + } + } + /// /// Looks up a localized string similar to Parent property path '{0}' could not be traversed.. /// diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.resx b/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.resx index 01b59dfe8833..d08a680ebae1 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.resx +++ b/src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.resx @@ -953,4 +953,10 @@ The header is followed by the list of parameters and their errors (might be seve Attempting to find json file '{0}' in '{1}' - + + The result of parsing the following JSON was 'null': + +{0} + {0} is the JSON that is being parsed. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/AddJsonPropertyPostActionProcessor.cs b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/AddJsonPropertyPostActionProcessor.cs index 80ca23402914..f517a6b363ba 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/AddJsonPropertyPostActionProcessor.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/AddJsonPropertyPostActionProcessor.cs @@ -19,7 +19,9 @@ internal class AddJsonPropertyPostActionProcessor : PostActionProcessorBase private const string ParentPropertyPathArgument = "parentPropertyPath"; private const string NewJsonPropertyNameArgument = "newJsonPropertyName"; private const string NewJsonPropertyValueArgument = "newJsonPropertyValue"; - private const string DetectRepoRootForFileCreation = "detectRepositoryRootForFileCreation"; + private const string DetectRepoRoot = "detectRepositoryRoot"; + private const string IncludeAllDirectoriesInSearch = "includeAllDirectoriesInSearch"; + private const string IncludeAllParentDirectoriesInSearch = "includeAllParentDirectoriesInSearch"; private static readonly JsonSerializerOptions SerializerOptions = new() { @@ -87,7 +89,33 @@ protected override bool ProcessInternal( return false; } - IReadOnlyList jsonFiles = FindFilesInCurrentFolderOrParentFolder(environment.Host.FileSystem, outputBasePath, jsonFileName); + if (!bool.TryParse(action.Args.GetValueOrDefault(DetectRepoRoot, "false"), out bool detectRepoRoot)) + { + Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, DetectRepoRoot)); + return false; + } + + if (!bool.TryParse(action.Args.GetValueOrDefault(IncludeAllDirectoriesInSearch, "true"), out bool includeAllDirectories)) + { + Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, IncludeAllDirectoriesInSearch)); + return false; + } + + string? repoRoot = detectRepoRoot ? GetRootDirectory(environment.Host.FileSystem, outputBasePath) : null; + + if (!bool.TryParse(action.Args.GetValueOrDefault(IncludeAllParentDirectoriesInSearch, "false"), out bool includeAllParentDirectories)) + { + Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, IncludeAllParentDirectoriesInSearch)); + return false; + } + + IReadOnlyList jsonFiles = FindFilesInCurrentFolderOrParentFolder( + environment.Host.FileSystem, + outputBasePath, + jsonFileName, + includeAllDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly, + includeAllParentDirectories ? int.MaxValue : 1, + repoRoot); if (jsonFiles.Count == 0) { @@ -103,13 +131,7 @@ protected override bool ProcessInternal( return false; } - if (!bool.TryParse(action.Args.GetValueOrDefault(DetectRepoRootForFileCreation, "false"), out bool detectRepoRoot)) - { - Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, DetectRepoRootForFileCreation)); - return false; - } - - string newJsonFilePath = Path.Combine(detectRepoRoot ? GetRootDirectory(environment.Host.FileSystem, outputBasePath) : outputBasePath, jsonFileName); + string newJsonFilePath = Path.Combine(repoRoot ?? outputBasePath, jsonFileName); environment.Host.FileSystem.WriteAllText(newJsonFilePath, "{}"); jsonFiles = new List { newJsonFilePath }; } @@ -150,17 +172,19 @@ protected override bool ProcessInternal( private static JsonNode? AddElementToJson(IPhysicalFileSystem fileSystem, string targetJsonFile, string? propertyPath, string propertyPathSeparator, string newJsonPropertyName, string newJsonPropertyValue, IPostAction action) { - JsonNode? jsonContent = JsonNode.Parse(fileSystem.ReadAllText(targetJsonFile), nodeOptions: null, documentOptions: DeserializerOptions); + var fileContent = fileSystem.ReadAllText(targetJsonFile); + JsonNode? jsonContent = JsonNode.Parse(fileContent, nodeOptions: null, documentOptions: DeserializerOptions); if (jsonContent == null) { + Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_NullJson, fileContent)); return null; } if (!bool.TryParse(action.Args.GetValueOrDefault(AllowPathCreationArgument, "false"), out bool createPath)) { Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, AllowPathCreationArgument)); - return false; + return null; } JsonNode? parentProperty = FindJsonNode(jsonContent, propertyPath, propertyPathSeparator, createPath); @@ -216,7 +240,10 @@ protected override bool ProcessInternal( private static string[] FindFilesInCurrentFolderOrParentFolder( IPhysicalFileSystem fileSystem, string startPath, - string matchPattern) + string matchPattern, + SearchOption searchOption, + int maxUpLevels, + string? repoRoot) { string? directory = fileSystem.DirectoryExists(startPath) ? startPath : Path.GetDirectoryName(startPath); @@ -230,17 +257,24 @@ private static string[] FindFilesInCurrentFolderOrParentFolder( do { Reporter.Verbose.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Verbose_AttemptingToFindJsonFile, matchPattern, directory)); - string[] filesInDir = fileSystem.EnumerateFileSystemEntries(directory, matchPattern, SearchOption.AllDirectories).ToArray(); + string[] filesInDir = fileSystem.EnumerateFileSystemEntries(directory, matchPattern, searchOption).ToArray(); if (filesInDir.Length > 0) { return filesInDir; } + if (repoRoot is not null && directory == repoRoot) + { + // The post action wants to detect the "repo root". + // We have already processed up to the repo root and didn't find any matching files, so we shouldn't go up any further. + return Array.Empty(); + } + directory = Path.GetPathRoot(directory) != directory ? Directory.GetParent(directory)?.FullName : null; numberOfUpLevels++; } - while (directory != null && numberOfUpLevels <= 1); + while (directory != null && numberOfUpLevels <= maxUpLevels); return Array.Empty(); } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.cs.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.cs.xlf index 45f6d58a1ba7..c65c0e782a66 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.cs.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve V řešení se nepovedlo najít soubor JSON. + + The result of parsing the following JSON was 'null': + +{0} + Výsledek analýzy následujícího kódu JSON byl null: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Cestu k nadřazené vlastnosti „{0}“ nelze projít. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.de.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.de.xlf index eb67ab8bf9bf..077b39b257a1 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.de.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Die JSON-Datei wurde in der Projektmappe nicht gefunden. + + The result of parsing the following JSON was 'null': + +{0} + Das Ergebnis der Analyse des folgenden JSON war „null“: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Der übergeordnete Eigenschaftenpfad "{0}" konnte nicht durchlaufen werden. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.es.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.es.xlf index 748e66f30a00..83e578cc9901 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.es.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve No se encuentra el archivo JSON en la solución + + The result of parsing the following JSON was 'null': + +{0} + El resultado del análisis del siguiente JSON fue "null": + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. No se pudo atravesar la ruta de acceso de la propiedad primaria '{0}'. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.fr.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.fr.xlf index d86722e36e60..321017f2a61c 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.fr.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Impossible de trouver le fichier json dans la solution + + The result of parsing the following JSON was 'null': + +{0} + Le résultat de l'analyse du JSON suivant était « null » : + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Le chemin de la propriété parente '{0}' n'a pas pu être traversé. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.it.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.it.xlf index 45d8e4f02e4e..37c751e7c39e 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.it.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Non è possibile trovare il file JSON nella soluzione + + The result of parsing the following JSON was 'null': + +{0} + Il risultato dell'analisi del file JSON seguente è 'null': + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Impossibile attraversare il percorso della proprietà padre '{0}'. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ja.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ja.xlf index 2f9a5c77a259..58b3201670e9 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ja.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve ソリューションで JSON ファイルが見つかりません + + The result of parsing the following JSON was 'null': + +{0} + 次の JSON の解析結果は 'null' でした: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. 親プロパティのパス '{0}' を走査できませんでした。 diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ko.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ko.xlf index 11699454506d..5367a26984e1 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ko.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve 솔루션에서 json 파일을 찾을 수 없습니다. + + The result of parsing the following JSON was 'null': + +{0} + 다음 JSON을 구문 분석한 결과는 'null'입니다. + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. '{0}' 부모 속성 경로를 트래버스할 수 없습니다. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pl.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pl.xlf index 6e5167124b31..35d4691fba5e 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pl.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Nie można odnaleźć pliku JSON w rozwiązaniu + + The result of parsing the following JSON was 'null': + +{0} + Wynikiem analizy następującego kodu JSON była wartość „null”: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Nie można przejść przez ścieżkę właściwości nadrzędnej „{0}”. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pt-BR.xlf index 24f89179cb85..accfeb0309e1 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.pt-BR.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Não é possível encontrar o arquivo json na solução + + The result of parsing the following JSON was 'null': + +{0} + O resultado da análise sintática do JSON a seguir foi 'null': + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. O caminho da propriedade pai '{0}' não pôde ser percorrido. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ru.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ru.xlf index 6578a4d5bf14..c5e680a97988 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.ru.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Не удалось найти файл JSON в решении + + The result of parsing the following JSON was 'null': + +{0} + Результат разбора следующего JSON оказался равен null: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Не удалось просмотреть путь к родительскому свойству "{0}". diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.tr.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.tr.xlf index a0d71cff2b96..ba424b44aa59 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.tr.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve Çözümde json dosyası bulunamıyor + + The result of parsing the following JSON was 'null': + +{0} + Aşağıdaki JSON'un ayrıştırma sonucu 'null' idi: + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. Üst özellik '{0}' yolu geçirilemedi. diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hans.xlf index f75d8ad68dae..e1820411d6c7 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hans.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve 在解决方案中找不到 json 文件 + + The result of parsing the following JSON was 'null': + +{0} + 解析以下 JSON 会得到 "null": + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. 无法遍历父属性路径“{0}”。 diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hant.xlf index 56c64a9d9b91..5030f71d78cc 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/Microsoft.TemplateEngine.Cli/xlf/LocalizableStrings.zh-Hant.xlf @@ -850,6 +850,15 @@ The header is followed by the list of parameters and their errors (might be seve 在解決方案中找不到 JSON 檔案 + + The result of parsing the following JSON was 'null': + +{0} + 剖析下列 JSON 的結果為 'null': + +{0} + {0} is the JSON that is being parsed. + Parent property path '{0}' could not be traversed. 無法周遊父屬性路徑 '{0}'。 diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx index 211be41cbab6..bd4864527b9c 100644 --- a/src/Cli/dotnet/CliStrings.resx +++ b/src/Cli/dotnet/CliStrings.resx @@ -817,7 +817,7 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is {Locked="dotnet workload update"} - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. Package reference id and version must not be null. diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 3a3fab487f89..74001a13dfe9 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -804,8 +804,9 @@ See https://aka.ms/dotnet-test/mtp for more information. Discovering tests from - - .NET Test Command + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Downloading pack {0} version {1} to offline cache {2}... @@ -1619,6 +1620,9 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Duplicate directives are not supported: {0} {0} is the directive type and name. + + Directives currently cannot contain double quotes ("). + Cannot specify option '{0}' when also using '-' to read the file from standard input. {0} is an option name like '--no-build'. @@ -1994,8 +1998,9 @@ Your project targets multiple frameworks. Specify which framework to run using ' Specify a temporary directory for this command to download and extract NuGet packages (must be secure). - - .NET Test Driver + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Run test(s), without displaying Microsoft Testplatform banner @@ -2698,4 +2703,25 @@ Proceed? Received 'ExecutionId' of value '{0}' for message '{1}' while the 'ExecutionId' received of the handshake message was '{2}'. {Locked="ExecutionId"} - \ No newline at end of file + + error: + + + total: + + + retried + + + failed: + + + succeeded: + + + skipped: + + + duration: + + diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs index 67c2a47f1051..3055de0883f5 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Reflection; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -14,6 +15,9 @@ public class MSBuildForwardingApp : CommandBase private readonly MSBuildForwardingAppWithoutLogging _forwardingAppWithoutLogging; + /// + /// Adds the CLI's telemetry logger to the MSBuild arguments if telemetry is enabled. + /// private static MSBuildArgs ConcatTelemetryLogger(MSBuildArgs msbuildArgs) { if (Telemetry.Telemetry.CurrentSessionId != null) @@ -45,8 +49,9 @@ public MSBuildForwardingApp(IEnumerable rawMSBuildArgs, string? msbuildP public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null, bool includeLogo = false) { + var modifiedMSBuildArgs = CommonRunHelpers.AdjustMSBuildForLLMs(ConcatTelemetryLogger(msBuildArgs)); _forwardingAppWithoutLogging = new MSBuildForwardingAppWithoutLogging( - ConcatTelemetryLogger(msBuildArgs), + modifiedMSBuildArgs, msbuildPath: msbuildPath, includeLogo: includeLogo); diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingLogger.cs index fc7c6488b910..fef3e251bedc 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingLogger.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingLogger.cs @@ -26,11 +26,14 @@ public void Initialize(IEventSource eventSource) eventSource4.IncludeEvaluationPropertiesAndItems(); } - // Only forward telemetry events + // Forward telemetry events if (eventSource is IEventSource2 eventSource2) { eventSource2.TelemetryLogged += (sender, args) => BuildEventRedirector.ForwardEvent(args); } + + // Forward build finished events. Is used for logging the aggregated build events. + eventSource.BuildFinished += (sender, args) => BuildEventRedirector.ForwardEvent(args); } public void Initialize(IEventSource eventSource, int nodeCount) diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs index 4f4f03506e15..265b1eafbb76 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; @@ -13,7 +13,7 @@ public sealed class MSBuildLogger : INodeLogger { private readonly IFirstTimeUseNoticeSentinel _sentinel = new FirstTimeUseNoticeSentinel(); - private readonly ITelemetry? _telemetry = null; + private readonly ITelemetry? _telemetry; internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval"; internal const string BuildTelemetryEventName = "build"; @@ -22,6 +22,10 @@ public sealed class MSBuildLogger : INodeLogger internal const string BuildcheckRunEventName = "buildcheck/run"; internal const string BuildcheckRuleStatsEventName = "buildcheck/rule"; + // These two events are aggregated and sent at the end of the build. + internal const string TaskFactoryTelemetryAggregatedEventName = "build/tasks/taskfactory"; + internal const string TasksTelemetryAggregatedEventName = "build/tasks"; + internal const string SdkTaskBaseCatchExceptionTelemetryEventName = "taskBaseCatchException"; internal const string PublishPropertiesTelemetryEventName = "PublishProperties"; internal const string WorkloadPublishPropertiesTelemetryEventName = "WorkloadPublishProperties"; @@ -48,6 +52,15 @@ public sealed class MSBuildLogger : INodeLogger /// internal const string SdkContainerPublishErrorEventName = "sdk/container/publish/error"; + /// + /// Stores aggregated telemetry data by event name and property name. + /// + /// + /// Key: event name, Value: property name to aggregated count. + /// Aggregation is very basic. Only integer properties are aggregated by summing values. Non-integer properties are ignored. + /// + private Dictionary> _aggregatedEvents = new(); + public MSBuildLogger() { try @@ -73,6 +86,14 @@ public MSBuildLogger() } } + /// + /// Constructor for testing purposes. + /// + internal MSBuildLogger(ITelemetry telemetry) + { + _telemetry = telemetry; + } + public void Initialize(IEventSource eventSource, int nodeCount) { Initialize(eventSource); @@ -95,7 +116,11 @@ public void Initialize(IEventSource eventSource) { eventSource2.TelemetryLogged += OnTelemetryLogged; } + + eventSource.BuildFinished += OnBuildFinished; } + + eventSource.BuildFinished += OnBuildFinished; } catch (Exception) { @@ -103,37 +128,100 @@ public void Initialize(IEventSource eventSource) } } + private void OnBuildFinished(object sender, BuildFinishedEventArgs e) + { + SendAggregatedEventsOnBuildFinished(_telemetry); + } + + internal void SendAggregatedEventsOnBuildFinished(ITelemetry? telemetry) + { + if (telemetry is null) return; + if (_aggregatedEvents.TryGetValue(TaskFactoryTelemetryAggregatedEventName, out var taskFactoryData)) + { + Dictionary taskFactoryProperties = ConvertToStringDictionary(taskFactoryData); + + TrackEvent(telemetry, $"msbuild/{TaskFactoryTelemetryAggregatedEventName}", taskFactoryProperties, toBeHashed: [], toBeMeasured: []); + _aggregatedEvents.Remove(TaskFactoryTelemetryAggregatedEventName); + } + + if (_aggregatedEvents.TryGetValue(TasksTelemetryAggregatedEventName, out var tasksData)) + { + Dictionary tasksProperties = ConvertToStringDictionary(tasksData); + + TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: [], toBeMeasured: []); + _aggregatedEvents.Remove(TasksTelemetryAggregatedEventName); + } + } + + private static Dictionary ConvertToStringDictionary(Dictionary properties) + { + Dictionary stringProperties = new(); + foreach (var kvp in properties) + { + stringProperties[kvp.Key] = kvp.Value.ToString(CultureInfo.InvariantCulture); + } + + return stringProperties; + } + + internal void AggregateEvent(TelemetryEventArgs args) + { + if (args.EventName is null) return; + if (!_aggregatedEvents.TryGetValue(args.EventName, out Dictionary? eventData) || eventData is null) + { + eventData = new Dictionary(); + _aggregatedEvents[args.EventName] = eventData; + } + + foreach (var kvp in args.Properties) + { + if (int.TryParse(kvp.Value, CultureInfo.InvariantCulture, out int count)) + { + if (!eventData.ContainsKey(kvp.Key)) + { + eventData[kvp.Key] = count; + } + else + { + eventData[kvp.Key] += count; + } + } + } + } + internal static void FormatAndSend(ITelemetry? telemetry, TelemetryEventArgs args) { switch (args.EventName) { case TargetFrameworkTelemetryEventName: - TrackEvent(telemetry, $"msbuild/{TargetFrameworkTelemetryEventName}", args.Properties, [], []); + TrackEvent(telemetry, $"msbuild/{TargetFrameworkTelemetryEventName}", args.Properties); break; case BuildTelemetryEventName: TrackEvent(telemetry, $"msbuild/{BuildTelemetryEventName}", args.Properties, toBeHashed: ["ProjectPath", "BuildTarget"], - toBeMeasured: ["BuildDurationInMilliseconds", "InnerBuildDurationInMilliseconds"]); + toBeMeasured: ["BuildDurationInMilliseconds", "InnerBuildDurationInMilliseconds"] + ); break; case LoggingConfigurationTelemetryEventName: TrackEvent(telemetry, $"msbuild/{LoggingConfigurationTelemetryEventName}", args.Properties, - toBeHashed: [], - toBeMeasured: ["FileLoggersCount"]); + toBeMeasured: ["FileLoggersCount"] + ); break; case BuildcheckAcquisitionFailureEventName: TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties, - toBeHashed: ["AssemblyName", "ExceptionType", "ExceptionMessage"], - toBeMeasured: []); + toBeHashed: ["AssemblyName", "ExceptionType", "ExceptionMessage"] + ); break; case BuildcheckRunEventName: TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties, - toBeHashed: [], - toBeMeasured: ["TotalRuntimeInMilliseconds"]); + toBeMeasured: ["TotalRuntimeInMilliseconds"] + ); break; case BuildcheckRuleStatsEventName: TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties, toBeHashed: ["RuleId", "CheckFriendlyName"], - toBeMeasured: ["TotalRuntimeInMilliseconds"]); + toBeMeasured: ["TotalRuntimeInMilliseconds"] + ); break; // Pass through events that don't need special handling case SdkTaskBaseCatchExceptionTelemetryEventName: @@ -143,7 +231,7 @@ internal static void FormatAndSend(ITelemetry? telemetry, TelemetryEventArgs arg case SdkContainerPublishBaseImageInferenceEventName: case SdkContainerPublishSuccessEventName: case SdkContainerPublishErrorEventName: - TrackEvent(telemetry, args.EventName, args.Properties, [], []); + TrackEvent(telemetry, args.EventName, args.Properties); break; default: // Ignore unknown events @@ -151,7 +239,7 @@ internal static void FormatAndSend(ITelemetry? telemetry, TelemetryEventArgs arg } } - private static void TrackEvent(ITelemetry? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed, string[]? toBeMeasured) + private static void TrackEvent(ITelemetry? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed = null, string[]? toBeMeasured = null) { if (telemetry == null || !telemetry.Enabled) { @@ -168,7 +256,7 @@ private static void TrackEvent(ITelemetry? telemetry, string eventName, IDiction if (eventProperties.TryGetValue(propertyToBeHashed, out var value)) { // Lets lazy allocate in case there is tons of telemetry - properties ??= new Dictionary(eventProperties); + properties ??= new(eventProperties); properties[propertyToBeHashed] = Sha256Hasher.HashWithNormalizedCasing(value!); } } @@ -178,10 +266,10 @@ private static void TrackEvent(ITelemetry? telemetry, string eventName, IDiction { foreach (var propertyToBeMeasured in toBeMeasured) { - if (eventProperties.TryGetValue(propertyToBeMeasured, out string? value)) + if (eventProperties.TryGetValue(propertyToBeMeasured, out var value)) { // Lets lazy allocate in case there is tons of telemetry - properties ??= new Dictionary(eventProperties); + properties ??= new(eventProperties); properties.Remove(propertyToBeMeasured); if (double.TryParse(value, CultureInfo.InvariantCulture, out double realValue)) { @@ -198,7 +286,14 @@ private static void TrackEvent(ITelemetry? telemetry, string eventName, IDiction private void OnTelemetryLogged(object sender, TelemetryEventArgs args) { - FormatAndSend(_telemetry, args); + if (args.EventName == TaskFactoryTelemetryAggregatedEventName || args.EventName == TasksTelemetryAggregatedEventName) + { + AggregateEvent(args); + } + else + { + FormatAndSend(_telemetry, args); + } } public void Shutdown() @@ -214,5 +309,6 @@ public void Shutdown() } public LoggerVerbosity Verbosity { get; set; } + public string? Parameters { get; set; } } diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 76bba800ff4a..f1bc3ab7d655 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -73,7 +73,8 @@ public override int Execute() using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write); using var writer = new StreamWriter(stream, Encoding.UTF8); VirtualProjectBuildingCommand.WriteProjectFile(writer, UpdateDirectives(directives), isVirtualProject: false, - userSecretsId: DetermineUserSecretsId()); + userSecretsId: DetermineUserSecretsId(), + excludeDefaultProperties: FindDefaultPropertiesToExclude()); } // Copy or move over included items. @@ -184,6 +185,18 @@ ImmutableArray UpdateDirectives(ImmutableArray return result.DrainToImmutable(); } + + IEnumerable FindDefaultPropertiesToExclude() + { + foreach (var (name, defaultValue) in VirtualProjectBuildingCommand.DefaultProperties) + { + string projectValue = projectInstance.GetPropertyValue(name); + if (!string.Equals(projectValue, defaultValue, StringComparison.OrdinalIgnoreCase)) + { + yield return name; + } + } + } } private string DetermineOutputDirectory(string file) diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs index e7762086e680..d53f7052b754 100644 --- a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs @@ -202,7 +202,6 @@ private IEnumerable GetCscArguments( "/deterministic+", "/langversion:14.0", "/features:FileBasedProgram", - $"/analyzerconfig:{SdkPath}/Sdks/Microsoft.NET.Sdk/codestyle/cs/build/config/analysislevelstyle_default.globalconfig", $"/analyzerconfig:{objDir}/{fileNameWithoutExtension}.GeneratedMSBuildEditorConfig.editorconfig", $"/analyzerconfig:{SdkPath}/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig", $"/analyzer:{SdkPath}/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll", diff --git a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs index eed2437e281c..64d2b8a1075a 100644 --- a/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs +++ b/src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs @@ -26,4 +26,29 @@ public static string GetPropertiesLaunchSettingsPath(string directoryPath, strin public static string GetFlatLaunchSettingsPath(string directoryPath, string projectNameWithoutExtension) => Path.Join(directoryPath, $"{projectNameWithoutExtension}.run.json"); + + + /// + /// Applies adjustments to MSBuild arguments to better suit LLM/agentic environments, if such an environment is detected. + /// + public static MSBuildArgs AdjustMSBuildForLLMs(MSBuildArgs msbuildArgs) + { + if (new Telemetry.LLMEnvironmentDetectorForTelemetry().IsLLMEnvironment()) + { + // disable the live-update display of the TerminalLogger, which wastes tokens + return msbuildArgs.CloneWithAdditionalArgs(Constants.TerminalLogger_DisableNodeDisplay); + } + else + { + return msbuildArgs; + } + } + + /// + /// Creates a TerminalLogger or ConsoleLogger based on the provided MSBuild arguments. + /// If the environment is detected to be an LLM environment, the logger is adjusted to + /// better suit that environment. + /// + public static Microsoft.Build.Framework.ILogger GetConsoleLogger(MSBuildArgs args) => + Microsoft.Build.Logging.TerminalLogger.CreateTerminalOrConsoleLogger([.. AdjustMSBuildForLLMs(args).OtherMSBuildArgs]); } diff --git a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs index 402da626784e..45fa0380aeea 100644 --- a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs +++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs @@ -86,7 +86,8 @@ private TextChange DetermineAddChange(CSharpDirective directive) { // Find one that has the same kind and name. // If found, we will replace it with the new directive. - if (directive is CSharpDirective.Named named && + var named = directive as CSharpDirective.Named; + if (named != null && Directives.OfType().FirstOrDefault(d => NamedDirectiveComparer.Instance.Equals(d, named)) is { } toReplace) { return new TextChange(toReplace.Info.Span, newText: directive.ToString() + NewLine); @@ -99,6 +100,14 @@ private TextChange DetermineAddChange(CSharpDirective directive) { if (existingDirective.GetType() == directive.GetType()) { + // Add named directives in sorted order. + if (named != null && + existingDirective is CSharpDirective.Named existingNamed && + string.CompareOrdinal(existingNamed.Name, named.Name) > 0) + { + break; + } + addAfter = existingDirective; } else if (addAfter != null) @@ -120,7 +129,7 @@ private TextChange DetermineAddChange(CSharpDirective directive) var result = tokenizer.ParseNextToken(); var leadingTrivia = result.Token.LeadingTrivia; - // If there is a comment at the top of the file, we add the directive after it + // If there is a comment or #! at the top of the file, we add the directive after it // (the comment might be a license which should always stay at the top). int insertAfterIndex = -1; int trailingNewLines = 0; @@ -161,6 +170,11 @@ private TextChange DetermineAddChange(CSharpDirective directive) } break; + case SyntaxKind.ShebangDirectiveTrivia: + trailingNewLines = 1; // shebang trivia has one newline embedded in its structure + insertAfterIndex = i; + break; + case SyntaxKind.EndOfLineTrivia: if (insertAfterIndex >= 0) { diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 183569417ae2..542fc76ffbcd 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -6,6 +6,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; @@ -483,7 +484,9 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs) { List loggersForBuild = [ - TerminalLogger.CreateTerminalOrConsoleLogger([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs]) + CommonRunHelpers.GetConsoleLogger( + buildArgs.CloneWithExplicitArgs([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs]) + ) ]; if (binaryLogger is not null) { @@ -497,6 +500,7 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca } } + [DoesNotReturn] internal static void ThrowUnableToRunError(ProjectInstance project) { string targetFrameworks = project.GetPropertyValue("TargetFrameworks"); diff --git a/src/Cli/dotnet/Commands/Run/RunProperties.cs b/src/Cli/dotnet/Commands/Run/RunProperties.cs index 81e92cb88315..6972d140ac56 100644 --- a/src/Cli/dotnet/Commands/Run/RunProperties.cs +++ b/src/Cli/dotnet/Commands/Run/RunProperties.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Utils; @@ -19,9 +20,9 @@ internal RunProperties(string command, string? arguments, string? workingDirecto { } - internal static RunProperties FromProject(ProjectInstance project) + internal static bool TryFromProject(ProjectInstance project, [NotNullWhen(returnValue: true)] out RunProperties? result) { - var result = new RunProperties( + result = new RunProperties( Command: project.GetPropertyValue("RunCommand"), Arguments: project.GetPropertyValue("RunArguments"), WorkingDirectory: project.GetPropertyValue("RunWorkingDirectory"), @@ -30,6 +31,17 @@ internal static RunProperties FromProject(ProjectInstance project) TargetFrameworkVersion: project.GetPropertyValue("TargetFrameworkVersion")); if (string.IsNullOrEmpty(result.Command)) + { + result = null; + return false; + } + + return true; + } + + internal static RunProperties FromProject(ProjectInstance project) + { + if (!TryFromProject(project, out var result)) { RunCommand.ThrowUnableToRunError(project); } diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs index 35e13b2d3fd2..1e58bdd27c05 100644 --- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs +++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs @@ -47,9 +47,13 @@ public static void TrackRunEvent( { ["app_type"] = isFileBased ? "file_based" : "project_based", ["project_id"] = projectIdentifier, - ["sdk_count"] = sdkCount.ToString(), - ["package_reference_count"] = packageReferenceCount.ToString(), - ["project_reference_count"] = projectReferenceCount.ToString(), + }; + + var measurements = new Dictionary + { + ["sdk_count"] = sdkCount, + ["package_reference_count"] = packageReferenceCount, + ["project_reference_count"] = projectReferenceCount, }; // Launch profile telemetry @@ -75,7 +79,7 @@ public static void TrackRunEvent( // File-based app specific telemetry if (isFileBased) { - properties["additional_properties_count"] = additionalPropertiesCount.ToString(); + measurements["additional_properties_count"] = additionalPropertiesCount; if (usedMSBuild.HasValue) { properties["used_msbuild"] = usedMSBuild.Value ? "true" : "false"; @@ -86,7 +90,7 @@ public static void TrackRunEvent( } } - TelemetryEventEntry.TrackEvent(RunEventName, properties, measurements: null); + TelemetryEventEntry.TrackEvent(RunEventName, properties, measurements); } /// @@ -234,4 +238,4 @@ private static bool IsDefaultProfile(string? profileName) // The default profile name at this point is "(Default)" return profileName.Equals("(Default)", StringComparison.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 8da06cc0a47f..5119a2b579df 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -75,7 +75,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase /// /// Kept in sync with the default dotnet new console project file (enforced by DotnetProjectAddTests.SameAsTemplate). /// - private static readonly FrozenDictionary s_defaultProperties = FrozenDictionary.Create(StringComparer.OrdinalIgnoreCase, + public static readonly FrozenDictionary DefaultProperties = FrozenDictionary.Create(StringComparer.OrdinalIgnoreCase, [ new("OutputType", "Exe"), new("TargetFramework", $"net{TargetFrameworkVersion}"), @@ -190,7 +190,7 @@ public override int Execute() var verbosity = MSBuildArgs.Verbosity ?? MSBuildForwardingAppWithoutLogging.DefaultVerbosity; var consoleLogger = minimizeStdOut ? new SimpleErrorLogger() - : TerminalLogger.CreateTerminalOrConsoleLogger([$"--verbosity:{verbosity}", .. MSBuildArgs.OtherMSBuildArgs]); + : CommonRunHelpers.GetConsoleLogger(MSBuildArgs.CloneWithExplicitArgs([$"--verbosity:{verbosity}", .. MSBuildArgs.OtherMSBuildArgs])); var binaryLogger = GetBinaryLogger(MSBuildArgs.OtherMSBuildArgs); CacheInfo? cache = null; @@ -354,7 +354,9 @@ public override int Execute() Debug.Assert(buildRequest.ProjectInstance != null); // Cache run info (to avoid re-evaluating the project instance). - cache.CurrentEntry.Run = RunProperties.FromProject(buildRequest.ProjectInstance); + cache.CurrentEntry.Run = RunProperties.TryFromProject(buildRequest.ProjectInstance, out var runProperties) + ? runProperties + : null; if (!MSBuildUtilities.ConvertStringToBool(buildRequest.ProjectInstance.GetPropertyValue(FileBasedProgramCanSkipMSBuild), defaultValue: true)) { @@ -899,7 +901,7 @@ private BuildLevel GetBuildLevel(out CacheInfo cache) { if (!NeedsToBuild(out cache)) { - Reporter.Verbose.WriteLine("No need to build, the output is up to date."); + Reporter.Verbose.WriteLine("No need to build, the output is up to date. Cache: " + ArtifactsPath); return BuildLevel.None; } @@ -1141,8 +1143,12 @@ public static void WriteProjectFile( string? targetFilePath = null, string? artifactsPath = null, bool includeRuntimeConfigInformation = true, - string? userSecretsId = null) + string? userSecretsId = null, + IEnumerable? excludeDefaultProperties = null) { + Debug.Assert(userSecretsId == null || !isVirtualProject); + Debug.Assert(excludeDefaultProperties == null || !isVirtualProject); + int processedDirectives = 0; var sdkDirectives = directives.OfType(); @@ -1181,6 +1187,20 @@ public static void WriteProjectFile( artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + """); + + // Write default properties before importing SDKs so they can be overridden by SDKs + // (and implicit build files which are imported by the default .NET SDK). + foreach (var (name, value) in DefaultProperties) + { + writer.WriteLine($""" + <{name}>{EscapeValue(value)} + """); + } + + writer.WriteLine($""" @@ -1247,34 +1267,30 @@ public static void WriteProjectFile( """); // First write the default properties except those specified by the user. - var customPropertyNames = propertyDirectives.Select(d => d.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); - foreach (var (name, value) in s_defaultProperties) + if (!isVirtualProject) { - if (!customPropertyNames.Contains(name)) + var customPropertyNames = propertyDirectives + .Select(static d => d.Name) + .Concat(excludeDefaultProperties ?? []) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var (name, value) in DefaultProperties) + { + if (!customPropertyNames.Contains(name)) + { + writer.WriteLine($""" + <{name}>{EscapeValue(value)} + """); + } + } + + if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId")) { writer.WriteLine($""" - <{name}>{EscapeValue(value)} + {EscapeValue(userSecretsId)} """); } } - if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId")) - { - writer.WriteLine($""" - {EscapeValue(userSecretsId)} - """); - } - - // Write virtual-only properties. - if (isVirtualProject) - { - writer.WriteLine(""" - false - true - false - """); - } - // Write custom properties. foreach (var property in propertyDirectives) { @@ -1289,6 +1305,7 @@ public static void WriteProjectFile( if (isVirtualProject) { writer.WriteLine(""" + false $(Features);FileBasedProgram """); } @@ -1490,8 +1507,15 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi Diagnostics = diagnostics, SourceFile = sourceFile, DirectiveKind = name.ToString(), - DirectiveText = value.ToString() + DirectiveText = value.ToString(), }; + + // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 + if (value.Contains('"')) + { + diagnostics.AddError(sourceFile, context.Info.Span, CliCommandStrings.QuoteInDirective); + } + if (CSharpDirective.Parse(context) is { } directive) { // If the directive is already present, report an error. @@ -1733,8 +1757,8 @@ public readonly struct ParseContext private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) { - var i = context.DirectiveText.IndexOf(separator, StringComparison.Ordinal); - var firstPart = (i < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(..i)).TrimEnd(); + var separatorIndex = context.DirectiveText.IndexOf(separator, StringComparison.Ordinal); + var firstPart = (separatorIndex < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(..separatorIndex)).TrimEnd(); string directiveKind = context.DirectiveKind; if (firstPart.IsWhiteSpace()) @@ -1748,12 +1772,20 @@ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.InvalidDirectiveName, directiveKind, separator)); } - var secondPart = i < 0 ? [] : context.DirectiveText.AsSpan((i + 1)..).TrimStart(); - if (i < 0 || secondPart.IsWhiteSpace()) + if (separatorIndex < 0) { return (firstPart.ToString(), null); } + var secondPart = context.DirectiveText.AsSpan((separatorIndex + 1)..).TrimStart(); + if (secondPart.IsWhiteSpace()) + { + Debug.Assert(secondPart.Length == 0, + "We have trimmed the second part, so if it's white space, it should be actually empty."); + + return (firstPart.ToString(), string.Empty); + } + return (firstPart.ToString(), secondPart.ToString()); } diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/HumanReadableDurationFormatter.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/HumanReadableDurationFormatter.cs index 559af6b77847..304166341323 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/HumanReadableDurationFormatter.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/HumanReadableDurationFormatter.cs @@ -16,6 +16,7 @@ public static void Append(ITerminal terminal, TimeSpan duration, bool wrapInPare terminal.Append('('); } + // TODO: Do these abbrevations (d for days, h for hours, etc) need to be localized? if (duration.Days > 0) { terminal.Append($"{duration.Days}d"); diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs index 3e320fa8a06b..22929d8dfb7f 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs @@ -280,13 +280,13 @@ private void AppendTestRunSummary(ITerminal terminal, int? exitCode) bool colorizePassed = passed > 0 && _buildErrorsCount == 0 && failed == 0 && error == 0; bool colorizeSkipped = skipped > 0 && skipped == total && _buildErrorsCount == 0 && failed == 0 && error == 0; - string errorText = $"{SingleIndentation}error: {error}"; - string totalText = $"{SingleIndentation}total: {total}"; - string retriedText = $" (+{retried} retried)"; - string failedText = $"{SingleIndentation}failed: {failed}"; - string passedText = $"{SingleIndentation}succeeded: {passed}"; - string skippedText = $"{SingleIndentation}skipped: {skipped}"; - string durationText = $"{SingleIndentation}duration: "; + string errorText = $"{SingleIndentation}{CliCommandStrings.ErrorColon} {error}"; + string totalText = $"{SingleIndentation}{CliCommandStrings.TotalColon} {total}"; + string retriedText = $" (+{retried} {CliCommandStrings.Retried})"; + string failedText = $"{SingleIndentation}{CliCommandStrings.FailedColon} {failed}"; + string passedText = $"{SingleIndentation}{CliCommandStrings.SucceededColon} {passed}"; + string skippedText = $"{SingleIndentation}{CliCommandStrings.SkippedColon} {skipped}"; + string durationText = $"{SingleIndentation}{CliCommandStrings.DurationColon} "; if (error > 0) { diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 0fb00c0de94d..db52e8975ed3 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.DotNet.Cli.Extensions; -using Microsoft.DotNet.Cli.Utils; using Command = System.CommandLine.Command; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -234,7 +233,7 @@ private static Command ConstructCommand() private static Command GetTestingPlatformCliCommand() { - var command = new MicrosoftTestingPlatformTestCommand("test", CliCommandStrings.DotnetTestCommand); + var command = new MicrosoftTestingPlatformTestCommand("test", CliCommandStrings.DotnetTestCommandMTPDescription); command.SetAction(parseResult => command.Run(parseResult)); command.Options.Add(MicrosoftTestingPlatformOptions.ProjectOption); command.Options.Add(MicrosoftTestingPlatformOptions.SolutionOption); @@ -268,7 +267,7 @@ private static Command GetTestingPlatformCliCommand() private static Command GetVSTestCliCommand() { - DocumentedCommand command = new("test", DocsLink, CliCommandStrings.TestAppFullName) + DocumentedCommand command = new("test", DocsLink, CliCommandStrings.DotnetTestCommandVSTestDescription) { TreatUnmatchedTokensAsErrors = false }; diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 51df89df08e0..a17cc0031e13 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Frozen; using System.CommandLine; using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommandParser.cs index f2c0109fe9fd..009b270947a3 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommandParser.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommandParser.cs @@ -10,7 +10,7 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.Install; internal static class ToolInstallCommandParser { - public static readonly Argument PackageIdentityArgument = CommonArguments.RequiredPackageIdentityArgument(); + public static readonly Argument PackageIdentityArgument = CommonArguments.RequiredPackageIdentityArgument("dotnetsay", "2.1.7"); public static readonly Option VersionOption = new("--version") { diff --git a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateCommandParser.cs b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateCommandParser.cs index 1bbb466a6af9..f29fce2f7086 100644 --- a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateCommandParser.cs +++ b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateCommandParser.cs @@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.Update; internal static class ToolUpdateCommandParser { - public static readonly Argument PackageIdentityArgument = CommonArguments.OptionalPackageIdentityArgument(); + public static readonly Argument PackageIdentityArgument = CommonArguments.OptionalPackageIdentityArgument("dotnetsay", "2.1.7"); public static readonly Option UpdateAllOption = ToolAppliedOption.UpdateAllOption; diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs index 3c6e0bb43c6d..32a38bdd9af9 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs @@ -174,10 +174,7 @@ private static Command ConstructCommand() private class ShowWorkloadsInfoAction : SynchronousCommandLineAction { - public ShowWorkloadsInfoAction() - { - Terminating = true; - } + public override bool Terminating => true; public override int Invoke(ParseResult parseResult) { @@ -189,10 +186,7 @@ public override int Invoke(ParseResult parseResult) private class ShowWorkloadsVersionOption : SynchronousCommandLineAction { - public ShowWorkloadsVersionOption() - { - Terminating = true; - } + public override bool Terminating => true; public override int Invoke(ParseResult parseResult) { diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index 9e6ea08c74b9..17ecd6053ba2 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -1162,10 +1162,15 @@ Další informace najdete na https://aka.ms/dotnet-test/mtp. Zjišťování testů z - - .NET Test Command - Testovací příkaz .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Testovací příkaz .NET pro Microsoft.Testing.Platform (vyjádřen výslovný souhlas prostřednictvím souboru global.json). Podporuje jenom Microsoft.Testing.Platform a nepodporuje VSTest. Další informace najdete na https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Testovací příkaz .NET pro VSTest. Pokud chcete použít Microsoft.Testing.Platform, prostřednictvím souboru global.json vyjádřete výslovný souhlas s příkazem založeným na Microsoft.Testing.Platform. Další informace najdete na https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Nastavte odlišné názvy profilů. Duplicitní direktivy nejsou podporovány: {0} {0} is the directive type and name. + + duration: + doba trvání: + + + + error: + chyba: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Nastavte odlišné názvy profilů. Nepovedlo se aktualizovat manifest reklamy {0}: {1}. + + failed: + neúspěšné: + + failed selhalo @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. Ve výchozím nastavení je publikována aplikace závislá na architektuře. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Vypne buildovací server Razor. @@ -2672,6 +2697,11 @@ Ve výchozím nastavení je publikována aplikace závislá na architektuře.Nástroj {0} (verze {1}) se obnovil. Dostupné příkazy: {2} + + retried + zkoušeno opakovaně + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Posune se na vyšší verzi architektury (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch Vynechá vytvoření souborů symbolů, které lze použít k profilaci optimalizovaných sestavení. + + skipped: + přeskočeno: + + skipped vynecháno @@ -3086,6 +3121,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch Cílový modul runtime pro uložení balíčků + + succeeded: + úspěšné: + + Summary Souhrn @@ -3101,11 +3141,6 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch Zadejte dočasný adresář pro tento příkaz, který se má stáhnout a extrahujte balíčky NuGet (musí být zabezpečené). - - .NET Test Driver - Testovací ovladač .NET - - + + total: + celkem: + + try {0} {0} pokus diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index 585628e6400b..e2bb95312d14 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -1162,10 +1162,15 @@ Weitere Informationen finden Sie unter https://aka.ms/dotnet-test/mtp. Tests ermitteln aus - - .NET Test Command - Testbefehl .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + .NET-Testbefehl für Microsoft.Testing.Platform (über die Datei „global.json“ aktiviert). Dies unterstützt ausschließlich Microsoft.Testing.Platform und nicht VSTest. Weitere Informationen finden Sie unter https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + .NET-Testbefehl für VSTest. Um Microsoft.Testing.Platform zu verwenden, aktivieren Sie den Microsoft.Testing.Platform-basierten Befehl über global.json. Weitere Informationen finden Sie unter https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Erstellen Sie eindeutige Profilnamen. Doppelte Anweisungen werden nicht unterstützt: {0} {0} is the directive type and name. + + duration: + Dauer: + + + + error: + Fehler: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Erstellen Sie eindeutige Profilnamen. Fehler beim Aktualisieren des Ankündigungsmanifests "{0}": {1}. + + failed: + fehlgeschlagen: + + failed fehlerhaft @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. Standardmäßig wird eine Framework-abhängige Anwendung veröffentlicht. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Hiermit wird der Razor-Buildserver heruntergefahren. @@ -2672,6 +2697,11 @@ Standardmäßig wird eine Framework-abhängige Anwendung veröffentlicht.Das Tool "{0}" (Version {1}) wurde wiederhergestellt. Verfügbare Befehle: {2} + + retried + Wiederholung + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Rollforward zu Frameworkversion (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches Hiermit wird die Erstellung von Symboldateien übersprungen, die für die Profilerstellung der optimierten Assemblys verwendet werden können. + + skipped: + übersprungen: + + skipped übersprungen @@ -3086,6 +3121,11 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches Die Zielruntime zum Speichern von Paketen. + + succeeded: + erfolgreich: + + Summary Zusammenfassung @@ -3101,11 +3141,6 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches Geben Sie ein temporäres Verzeichnis für diesen Befehl zum Herunterladen und Extrahieren von NuGet-Paketen an (muss sicher sein). - - .NET Test Driver - .NET-Testtreiber - - + + total: + gesamt: + + try {0} {0} testen diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index 3860162de57e..2446c0253eba 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -1162,10 +1162,15 @@ Consulte https://aka.ms/dotnet-test/mtp para obtener más información. Detección de pruebas de - - .NET Test Command - Comando de prueba de .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Comando de prueba de .NET para Microsoft.Testing.Platform (activado mediante el archivo “global.json”). Solo es compatible con Microsoft.Testing.Platform y no con VSTest. Consulte https://aka.ms/dotnet-test para obtener más información. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Comando de prueba de .NET para VSTest. Para usar Microsoft.Testing.Platform, actívelo en el comando basado en Microsoft.Testing.Platform mediante global.json. Consulte https://aka.ms/dotnet-test para obtener más información. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Defina nombres de perfiles distintos. No se admiten directivas duplicadas: {0} {0} is the directive type and name. + + duration: + duración: + + + + error: + error: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Defina nombres de perfiles distintos. No se pudo actualizar el manifiesto publicitario {0}: {1}. + + failed: + error: + + failed con errores @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. El valor predeterminado es publicar una aplicación dependiente del marco. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Apaga el servidor de compilación de Razor. @@ -2672,6 +2697,11 @@ El valor predeterminado es publicar una aplicación dependiente del marco.Se restauró la herramienta "{0}" (versión "{1}"). Comandos disponibles: {2} + + retried + volver a intentarlo + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Reenviar a la versión del marco (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa Omite la creación de archivos de símbolos que se pueden usar para generar perfiles para los ensamblados optimizados. + + skipped: + omitido: + + skipped omitido @@ -3086,6 +3121,11 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa El entorno tiempo de ejecución de destino para el que se almacenan los paquetes. + + succeeded: + correcto: + + Summary Resumen @@ -3101,11 +3141,6 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa Especifique un directorio temporal para que este comando descargue y extraiga paquetes NuGet (debe ser seguro). - - .NET Test Driver - Controlador de pruebas de .NET - - + + total: + total: + + try {0} intento {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 82c93100c2a2..710630a9a7f5 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -1162,10 +1162,15 @@ Pour découvrir plus d’informations, consultez https://aka.ms/dotnet-test/mtp. Découverte des tests à partir de - - .NET Test Command - Commande de test .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Commande de test .NET pour Microsoft.Testing.Platform (activée via le fichier « global.json »). Cela prend uniquement en charge Microsoft.Testing.Platform et ne prend pas en charge VSTest. Pour plus d'informations, consultez https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Commande de test .NET pour VSTest. Pour utiliser Microsoft.Testing.Platform, optez pour la commande basée sur Microsoft.Testing.Platform via global.json. Pour plus d'informations, consultez https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Faites en sorte que les noms de profil soient distincts. Les directives dupliquées ne sont pas prises en charge : {0} {0} is the directive type and name. + + duration: + durée : + + + + error: + erreur : + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Faites en sorte que les noms de profil soient distincts. Échec de la mise à jour du manifeste de publicité {0} : {1}. + + failed: + échec : + + failed échec @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. La valeur par défaut est de publier une application dépendante du framework. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Arrêtez le serveur de builds Razor. @@ -2672,6 +2697,11 @@ La valeur par défaut est de publier une application dépendante du framework.L'outil '{0}' (version '{1}') a été restauré. Commandes disponibles : {2} + + retried + réessayé + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Restaurer par progression la version du framework (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à Ignorez la création de fichiers de symboles pour le profilage des assemblys optimisés. + + skipped: + ignoré : + + skipped ignoré @@ -3086,6 +3121,11 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à Runtime cible pour lequel le stockage des packages est effectué. + + succeeded: + réussie : + + Summary Récapitulatif @@ -3101,11 +3141,6 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à Spécifiez un répertoire temporaire pour que cette commande télécharge et extrait les packages NuGet (doit être sécurisé). - - .NET Test Driver - Pilote de test .NET - - + + total: + total : + + try {0} essayer {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index d5e325548a52..69b69e641826 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -1162,10 +1162,15 @@ Per altre informazioni, vedere https://aka.ms/dotnet-test/mtp. Individuazione di test da - - .NET Test Command - Comando di test .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Comando di test .NET per Microsoft.Testing.Platform (attivato tramite il file 'global.json'). Supporta solo Microsoft.Testing.Platform e non VSTest. Per altre informazioni, vedere https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Comando di test .NET per VSTest. Per usare Microsoft.Testing.Platform, attivare il comando basato su Microsoft.Testing.Platform tramite global.json. Per altre informazioni, vedere https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Rendi distinti i nomi profilo. Le direttive duplicate non supportate: {0} {0} is the directive type and name. + + duration: + durata: + + + + error: + errore: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Rendi distinti i nomi profilo. Non è stato possibile scaricare il manifesto di pubblicità {0}: {1}. + + failed: + non riuscito: + + failed operazione non riuscita @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. Per impostazione predefinita, viene generato un pacchetto dipendente dal framework. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Arresta il server di compilazione di Razor. @@ -2672,6 +2697,11 @@ Per impostazione predefinita, viene generato un pacchetto dipendente dal framewo Lo strumento '{0}' (versione '{1}') è stato ripristinato. Comandi disponibili: {2} + + retried + ripetuto + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Esegue il roll forward alla versione del framework (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire Non crea i file di simboli che è possibile usare per la profilatura degli assembly ottimizzati. + + skipped: + ignorato: + + skipped ignorato @@ -3086,6 +3121,11 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire Runtime di destinazione per cui archiviare i pacchetti. + + succeeded: + operazione completata: + + Summary Riepilogo @@ -3101,11 +3141,6 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire Specificare una directory temporanea per questo comando per scaricare ed estrarre i pacchetti NuGet (deve essere protetta). - - .NET Test Driver - Driver di test .NET - - + + total: + totale: + + try {0} prova {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index d094f986fe57..4a83f985e878 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -1162,10 +1162,15 @@ See https://aka.ms/dotnet-test/mtp for more information. からテストを検出しています - - .NET Test Command - .NET Test コマンド - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Microsoft.Testing.Platform 用の .NET テスト コマンド ('global.json' ファイルでオプトイン済み)。これは Microsoft.Testing.Platform のみをサポートしており、VSTest には対応していません。詳細については、https://aka.ms/dotnet-test をご覧ください。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + VSTest 用の .NET テスト コマンド。Microsoft.Testing.Platform を使用するには、global.json ファイルで Microsoft.Testing.Platform ベースのコマンドにオプトインしてください。詳細については、https://aka.ms/dotnet-test をご覧ください。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Make the profile names distinct. 重複するディレクティブはサポートされていません: {0} {0} is the directive type and name. + + duration: + 期間: + + + + error: + エラー: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Make the profile names distinct. 広告マニフェスト {0} を更新できませんでした: {1}。 + + failed: + 失敗: + + failed 失敗 @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. 既定では、フレームワークに依存したアプリケーションが公開されます。 + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Razor ビルド サーバーをシャットダウンします。 @@ -2672,6 +2697,11 @@ The default is to publish a framework-dependent application. ツール '{0}' (バージョン '{1}') は復元されました。使用できるコマンド: {2} + + retried + 再試行済み + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). フレームワーク バージョン (LatestPatch、Minor、LatestMinor、Major、LatestMajor、Disable) にロールフォワードします。 @@ -2951,6 +2981,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 最適化されたアセンブリのプロファイルに使用できるシンボル ファイルの作成をスキップします。 + + skipped: + スキップ済み: + + skipped スキップされました @@ -3086,6 +3121,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' パッケージを格納するターゲット ランタイム。 + + succeeded: + 成功: + + Summary 概要 @@ -3101,11 +3141,6 @@ Your project targets multiple frameworks. Specify which framework to run using ' このコマンドに NuGet パッケージをダウンロードして抽出するための一時ディレクトリを指定します (セキュリティで保護する必要があります)。 - - .NET Test Driver - .NET Test Driver - - + + total: + 合計: + + try {0} {0} を試す diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index f818c10fae6d..f96a7af1b6fa 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -1162,10 +1162,15 @@ See https://aka.ms/dotnet-test/mtp for more information. 다음에서 테스트 검색하는 중 - - .NET Test Command - .NET 테스트 명령 - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Microsoft.Testing.Platform용 .NET 테스트 명령입니다('global.json' 파일을 통해 옵트인). 이는 Microsoft.Testing.Platform만 지원하며 VSTest를 지원하지 않습니다. 자세한 내용은 https://aka.ms/dotnet-test를 참조하세요. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + VSTest에 대한 .NET 테스트 명령입니다. Microsoft.Testing.Platform을 사용하려면 global.json을 통해 Microsoft.Testing.Platform 기반 명령을 옵트인합니다. 자세한 내용은 https://aka.ms/dotnet-test를 참조하세요. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Make the profile names distinct. 중복 지시문은 지원되지 않습니다. {0} {0} is the directive type and name. + + duration: + 기간: + + + + error: + 오류: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Make the profile names distinct. 광고 매니페스트 {0}: {1}을(를) 업데이트하지 못했습니다. + + failed: + 실패: + + failed 실패 @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. 기본값은 프레임워크 종속 애플리케이션을 게시하는 것입니다. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Razor 빌드 서버를 종료합니다. @@ -2672,6 +2697,11 @@ The default is to publish a framework-dependent application. '{0}' 도구(버전 '{1}')가 복원되었습니다. 사용 가능한 명령: {2} + + retried + 다시 시도됨 + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). 프레임워크 버전(LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable)으로 롤포워드합니다. @@ -2951,6 +2981,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 최적화된 어셈블리 프로파일링에 대해 사용할 수 있는 기호 파일 만들기를 건너뜁니다. + + skipped: + 건너뜀: + + skipped 건너뜀 @@ -3086,6 +3121,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 패키지를 저장할 대상 런타임입니다. + + succeeded: + 성공: + + Summary 요약 @@ -3101,11 +3141,6 @@ Your project targets multiple frameworks. Specify which framework to run using ' NuGet 패키지를 다운로드하고 추출하려면 이 명령의 임시 디렉터리를 지정합니다(보안이 있어야 합니다). - - .NET Test Driver - .NET 테스트 드라이버 - - + + total: + 합계: + + try {0} {0} 시도 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 100304d2eee0..e88469c1efce 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -1162,10 +1162,15 @@ Aby uzyskać więcej informacji, zobacz https://aka.ms/dotnet-test/mtp. Odnajdywanie testów w - - .NET Test Command - Polecenie testowe platformy .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Polecenie testowe platformy .NET dla elementu Microsoft.Testing.Platform (aktywowane przez plik „global.json”). Obsługuje tylko Microsoft.Testing.Platform i nie obsługuje VSTest. Aby uzyskać więcej informacji, zobacz https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Polecenie testowe platformy .NET dla narzędzia VSTest. Aby korzystać z platformy Microsoft.Testing.Platform, wyraź zgodę na użycie polecenia opartego na Microsoft.Testing.Platform za pośrednictwem pliku global.json. Aby uzyskać więcej informacji, zobacz https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Rozróżnij nazwy profilów. Zduplikowane dyrektywy nie są obsługiwane: {0} {0} is the directive type and name. + + duration: + czas trwania: + + + + error: + błąd: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Rozróżnij nazwy profilów. Nie można zaktualizować manifestu anonsowania {0}: {1}. + + failed: + zakończone niepowodzeniem: + + failed zakończone niepowodzeniem @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. Domyślnie publikowana jest aplikacja zależna od struktury. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Zamknij serwer kompilacji Razor. @@ -2672,6 +2697,11 @@ Domyślnie publikowana jest aplikacja zależna od struktury. Narzędzie „{0}” (wersja „{1}”) zostało przywrócone. Dostępne polecenia: {2} + + retried + próbowano ponownie + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Przewiń do wersji platformy (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u Pomiń tworzenie plików symboli, za pomocą których można profilować zoptymalizowane zestawy. + + skipped: + pominięto: + + skipped pominięte @@ -3086,6 +3121,11 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u Docelowe środowisko uruchomieniowe, dla którego mają być przechowywane pakiety. + + succeeded: + zakończone powodzeniem: + + Summary Podsumowanie @@ -3101,11 +3141,6 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u Określ katalog tymczasowy dla tego polecenia, aby pobrać i wyodrębnić pakiety NuGet (musi być bezpieczny). - - .NET Test Driver - Sterownik testów platformy .NET - - + + total: + suma: + + try {0} wypróbuj {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 2e8c726a75a1..52b11f5c695f 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -1162,10 +1162,15 @@ Consulte https://aka.ms/dotnet-test/mtp para obter mais informações. Descobrindo testes de - - .NET Test Command - Comando de Teste do .NET - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Comando de Teste .NET para Microsoft.Testing.Platform (aceito por meio do arquivo 'global.json'). Isso dá suporte apenas a Microsoft.Testing.Platform e não dá suporte a VSTest. Para obter mais informações, confira https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Comando de Teste .NET para VSTest. Para usar a Microsoft.Testing.Platform, faça a aceitação do comando baseado em Microsoft.Testing.Platform por meio do global.json. Para obter mais informações, confira https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Diferencie os nomes dos perfis. Diretivas duplicadas não são suportadas:{0} {0} is the directive type and name. + + duration: + duração: + + + + error: + erro: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Diferencie os nomes dos perfis. Falha ao atualizar o manifesto de publicidade {0}: {1}. + + failed: + falhou: + + failed com falha @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. O padrão é publicar uma aplicação dependente de framework. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Desligar o servidor de build do Razor. @@ -2672,6 +2697,11 @@ O padrão é publicar uma aplicação dependente de framework. A ferramenta '{0}' (versão '{1}') foi restaurada. Comandos disponíveis: {2} + + retried + repetido + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Role para frente para a versão de estrutura (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa Ignorar a criação de arquivos de símbolo que podem ser usados para criar o perfil de assemblies otimizados. + + skipped: + ignorados: + + skipped ignorado @@ -3086,6 +3121,11 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa O runtime de destino no qual os pacotes serão armazenados. + + succeeded: + concluído com êxito: + + Summary Resumo @@ -3101,11 +3141,6 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa Especifique um diretório temporário para este comando baixar e extrair pacotes NuGet (deve ser seguro). - - .NET Test Driver - Driver de Teste do .NET - - + + total: + total: + + try {0} experimente {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 391f4d068e0c..bab1f4575a7d 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -1162,10 +1162,15 @@ See https://aka.ms/dotnet-test/mtp for more information. Обнаружение тестов из - - .NET Test Command - Команда .NET Test - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Тестовая команда .NET для Microsoft.Testing.Platform (предоставлено согласие с помощью файла global.json). Поддерживается только Microsoft.Testing.Platform, VSTest не поддерживается. Дополнительные сведения см. на странице https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + Тестовая команда .NET для VSTest. Чтобы использовать Microsoft.Testing.Platform, согласитесь на использование команды на основе Microsoft.Testing.Platform посредством global.json. Дополнительные сведения см. на странице https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Make the profile names distinct. Повторяющиеся директивы не поддерживаются: {0} {0} is the directive type and name. + + duration: + длительность: + + + + error: + ошибка: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Make the profile names distinct. Не удалось обновить манифест рекламы {0}: {1}. + + failed: + сбой: + + failed сбой @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. По умолчанию публикация выполняется в приложение, зависимое от платформы. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Завершает работу сервера сборки Razor. @@ -2672,6 +2697,11 @@ The default is to publish a framework-dependent application. Средство "{0}" (версия "{1}") было восстановлено. Доступные команды: {2} + + retried + повторено + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Накат до версии платформы (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). @@ -2951,6 +2981,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' Пропуск создания файлов символов, которые можно использовать для профилирования оптимизированных сборок. + + skipped: + пропущено: + + skipped пропущено @@ -3086,6 +3121,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' Целевая среда выполнения, для которой хранятся пакеты. + + succeeded: + успешно: + + Summary Сводка @@ -3101,11 +3141,6 @@ Your project targets multiple frameworks. Specify which framework to run using ' Укажите временный каталог для этой команды, чтобы скачать и извлечь пакеты NuGet (должны быть защищены). - - .NET Test Driver - Драйвер тестов .NET - - + + total: + итог: + + try {0} попробуйте {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index f6ffea6d2984..1f1f787d8afa 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -1162,10 +1162,15 @@ Daha fazla bilgi için https://aka.ms/dotnet-test/mtp adresine bakın. Testler şuradan bulunuyor: - - .NET Test Command - .NET Test Komutu - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + Microsoft.Testing.Platform için .NET Test Komutu ('global.json' dosyasıyla seçildi). Bu sadece Microsoft.Testing.Platform'u destekler, VSTest'i desteklemez. Daha fazla bilgi için bkz. https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + VSTest için .NET Test Komutu. Microsoft.Testing.Platform'u kullanmak için global.json üzerinden Microsoft.Testing.Platform tabanlı komutu seçin. Daha fazla bilgi için bkz. https://aka.ms/dotnet-test. + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Lütfen profil adlarını değiştirin. Yinelenen yönergeler desteklenmez: {0} {0} is the directive type and name. + + duration: + süre: + + + + error: + hata: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Lütfen profil adlarını değiştirin. Reklam bildirimi {0} güncelleştirilemedi: {1}. + + failed: + başarısız oldu: + + failed başarısız @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. Varsayılan durum, çerçeveye bağımlı bir uygulama yayımlamaktır. + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. Razor derleme sunucusunu kapatır. @@ -2672,6 +2697,11 @@ Varsayılan durum, çerçeveye bağımlı bir uygulama yayımlamaktır. '{0}' aracı (sürüm '{1}') geri yüklendi. Kullanılabilen komutlar: {2} + + retried + yeniden denendi + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). Şu framework sürümüne ileri sarın: (LatestPatch, İkincil, LatestMinor, Ana, LatestMajor, Devre dışı). @@ -2951,6 +2981,11 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' İyileştirilen bütünleştirilmiş kodların profilini oluşturmak için kullanılabilen sembol dosyalarını oluşturma işlemini atlar. + + skipped: + atlandı: + + skipped atlandı @@ -3086,6 +3121,11 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' Paketlerin geri yükleneceği hedef çalışma zamanı. + + succeeded: + başarılı: + + Summary Özet @@ -3101,11 +3141,6 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' Bu komut için NuGet paketlerini indirmek ve ayıklamak üzere geçici bir dizin belirtin (güvenli olmalıdır). - - .NET Test Driver - .NET Test Sürücüsü - - + + total: + toplam: + + try {0} Şunu deneyin: {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 7878f6ad146e..bf50f4c94c52 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -1162,10 +1162,15 @@ See https://aka.ms/dotnet-test/mtp for more information. 正在发现以下位置中的测试 - - .NET Test Command - .NET 测试命令 - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + 适用于 Microsoft.Testing.Platform 的 .NET 测试命令(已通过 "global.json" 文件选择加入)。此命令仅支持 Microsoft.Testing.Platform,不支持 VSTest。有关详细信息,请参阅 https://aka.ms/dotnet-test。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + 适用于 VSTest 的 .NET 测试命令。若要使用 Microsoft.Testing.Platform,请通过 global.json 选择加入基于 Microsoft.Testing.Platform 的命令。有关详细信息,请参阅 https://aka.ms/dotnet-test。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Make the profile names distinct. 不支持重复指令: {0} {0} is the directive type and name. + + duration: + 持续时间: + + + + error: + 错误: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Make the profile names distinct. 未能更新广告清单 {0}: {1}。 + + failed: + 失败: + + failed 失败 @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. 默认情况下发布依赖于框架的应用程序。 + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. 关闭 Razor 生成服务器。 @@ -2672,6 +2697,11 @@ The default is to publish a framework-dependent application. 工具“{0}”(版本“{1}”)已还原。可用的命令: {2} + + retried + 已重试 + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). 前滚至框架版本(LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable)。 @@ -2951,6 +2981,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 跳过符号文件的创建操作,这些文件可用于分析已优化的程序集。 + + skipped: + 已跳过: + + skipped 已跳过 @@ -3086,6 +3121,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 要存储包的目标运行时。 + + succeeded: + 成功: + + Summary 摘要 @@ -3101,11 +3141,6 @@ Your project targets multiple frameworks. Specify which framework to run using ' 为此命令指定一个临时目录,以下载并提取(必须安全)的 NuGet 包。 - - .NET Test Driver - .NET 测试驱动程序 - - + + total: + 总计: + + try {0} 尝试 {0} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index 5cc582623c6d..31bed5db1839 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -1162,10 +1162,15 @@ See https://aka.ms/dotnet-test/mtp for more information. 正在以下位置找測試 - - .NET Test Command - .NET 測試命令 - + + .NET Test Command for Microsoft.Testing.Platform (opted-in via 'global.json' file). This only supports Microsoft.Testing.Platform and doesn't support VSTest. For more information, see https://aka.ms/dotnet-test. + .NET 測試命令,適用於 Microsoft.Testing.Platform (透過 'global.json' 檔案選擇加入)。此命令僅支援 Microsoft.Testing.Platform,不支援 VSTest。如需詳細資訊,請參閱 https://aka.ms/dotnet-test。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} + + + .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. + 適用於 VSTest 的 .NET 測試命令。若要使用 Microsoft.Testing.Platform,請透過 global.json 選擇加入以 Microsoft.Testing.Platform 為基礎的命令。如需詳細資訊,請參閱 https://aka.ms/dotnet-test。 + {Locked="global.json"}{Locked="Microsoft.Testing.Platform"}{Locked="VSTest"} Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible. @@ -1231,6 +1236,16 @@ Make the profile names distinct. 不支援重複的指示詞: {0} {0} is the directive type and name. + + duration: + 期間: + + + + error: + 錯誤: + + The following exception occurred when running the test module with RunCommand '{0}' and RunArguments '{1}': @@ -1281,6 +1296,11 @@ Make the profile names distinct. 無法更新廣告資訊清單 {0}: {1}。 + + failed: + 失敗: + + failed 已失敗 @@ -2552,6 +2572,11 @@ The default is to publish a framework-dependent application. 預設為發佈與 Framework 相依的應用程式。 + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + Shut down the Razor build server. 關閉 Razor 組建伺服器。 @@ -2672,6 +2697,11 @@ The default is to publish a framework-dependent application. 已還原工具 '{0}' (版本 '{1}')。可用的命令: {2} + + retried + 已重試 + + Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable). 復原為架構版本 (LatestPatch、Minor、LatestMinor、Major、LatestMajor、Disable)。 @@ -2951,6 +2981,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 跳過建立符號檔,該檔案可用於分析最佳化組件。 + + skipped: + 已略過: + + skipped 已跳過 @@ -3086,6 +3121,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 要對其儲存套件的目標執行階段。 + + succeeded: + 已成功: + + Summary 摘要 @@ -3101,11 +3141,6 @@ Your project targets multiple frameworks. Specify which framework to run using ' 指定此命令的暫存目錄,以下載並解壓縮 NuGet 套件 (必須為安全)。 - - .NET Test Driver - .NET 測試驅動程式 - - + + total: + 總計: + + try {0} 嘗試 {0} diff --git a/src/Cli/dotnet/CommonArguments.cs b/src/Cli/dotnet/CommonArguments.cs index 994444113cbd..bef42994416f 100644 --- a/src/Cli/dotnet/CommonArguments.cs +++ b/src/Cli/dotnet/CommonArguments.cs @@ -12,17 +12,23 @@ namespace Microsoft.DotNet.Cli internal class CommonArguments { public static DynamicArgument OptionalPackageIdentityArgument() => + OptionalPackageIdentityArgument("Newtonsoft.Json", "13.0.3"); + + public static DynamicArgument OptionalPackageIdentityArgument(string examplePackage, string exampleVersion) => new("packageId") { - Description = CliStrings.PackageIdentityArgumentDescription, + Description = string.Format(CliStrings.PackageIdentityArgumentDescription, examplePackage, exampleVersion), CustomParser = (ArgumentResult argumentResult) => ParsePackageIdentityWithVersionSeparator(argumentResult.Tokens[0]?.Value), Arity = ArgumentArity.ZeroOrOne, }; public static DynamicArgument RequiredPackageIdentityArgument() => + RequiredPackageIdentityArgument("Newtonsoft.Json", "13.0.3"); + + public static DynamicArgument RequiredPackageIdentityArgument(string examplePackage, string exampleVersion) => new("packageId") { - Description = CliStrings.PackageIdentityArgumentDescription, + Description = string.Format(CliStrings.PackageIdentityArgumentDescription, examplePackage, exampleVersion), CustomParser = (ArgumentResult argumentResult) => ParsePackageIdentityWithVersionSeparator(argumentResult.Tokens[0]?.Value)!.Value, Arity = ArgumentArity.ExactlyOne, }; diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs index 4bddb07f976e..23dd3d6ebef3 100644 --- a/src/Cli/dotnet/Parser.cs +++ b/src/Cli/dotnet/Parser.cs @@ -427,10 +427,8 @@ public override void Write(HelpContext context) private class PrintCliSchemaAction : SynchronousCommandLineAction { - internal PrintCliSchemaAction() - { - Terminating = true; - } + public override bool Terminating => true; + public override int Invoke(ParseResult parseResult) { CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.InvocationConfiguration.Output, Program.TelemetryClient); diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index cd82a15330f4..e57fe3a5635f 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -6,6 +6,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.DotNet.Cli.CommandFactory; using Microsoft.DotNet.Cli.CommandFactory.CommandResolution; using Microsoft.DotNet.Cli.Commands.Run; @@ -29,6 +30,10 @@ public class Program public static ITelemetry TelemetryClient; public static int Main(string[] args) { + // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix. + // See https://github.com/dotnet/docs/issues/46226. + using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0)); + using AutomaticEncodingRestorer _ = new(); // Setting output encoding is not available on those platforms @@ -235,10 +240,7 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) if (TelemetryClient.Enabled) { // Get the global.json state to report in telemetry along with this command invocation. - // We don't care about the actual SDK resolution, just the global.json information, - // so just pass empty string as executable directory for resolution. - NativeWrapper.SdkResolutionResult result = NativeWrapper.NETCoreSdkResolverNativeWrapper.ResolveSdk(string.Empty, Environment.CurrentDirectory); - globalJsonState = result.GlobalJsonState; + globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory); } TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData, globalJsonState)); diff --git a/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs index 5cd73f53abb8..ebdf6321ddd7 100644 --- a/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs +++ b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Telemetry; @@ -33,8 +34,7 @@ public BooleanEnvironmentRule(params string[] variables) public override bool IsMatch() { - return _variables.Any(variable => - bool.TryParse(Environment.GetEnvironmentVariable(variable), out bool value) && value); + return _variables.Any(variable => Env.GetEnvironmentVariableAsBool(variable)); } } @@ -81,12 +81,12 @@ public override bool IsMatch() /// The type of the result value. internal class EnvironmentDetectionRuleWithResult where T : class { - private readonly string[] _variables; + private readonly EnvironmentDetectionRule _rule; private readonly T _result; - public EnvironmentDetectionRuleWithResult(T result, params string[] variables) + public EnvironmentDetectionRuleWithResult(T result, EnvironmentDetectionRule rule) { - _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + _rule = rule ?? throw new ArgumentNullException(nameof(rule)); _result = result ?? throw new ArgumentNullException(nameof(result)); } @@ -96,8 +96,8 @@ public EnvironmentDetectionRuleWithResult(T result, params string[] variables) /// The result value if the rule matches; otherwise, null. public T? GetResult() { - return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) - ? _result + return _rule.IsMatch() + ? _result : null; } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs index fe599569aa6c..e2ee21591567 100644 --- a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs +++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs @@ -5,5 +5,13 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal interface ILLMEnvironmentDetector { + /// + /// Checks the current environment for known indicators of LLM usage and returns a string identifying the LLM environment if detected. + /// string? GetLLMEnvironment(); -} \ No newline at end of file + + /// + /// Returns true if the current environment is detected to be an LLM/agentic environment, false otherwise. + /// + bool IsLLMEnvironment(); +} diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index 16d13a6879e7..b37f9b5d0830 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -1,23 +1,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Linq; - namespace Microsoft.DotNet.Cli.Telemetry; internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [ // Claude Code - new EnvironmentDetectionRuleWithResult("claude", "CLAUDECODE"), + new EnvironmentDetectionRuleWithResult("claude", new AnyPresentEnvironmentRule("CLAUDECODE")), // Cursor AI - new EnvironmentDetectionRuleWithResult("cursor", "CURSOR_EDITOR") + new EnvironmentDetectionRuleWithResult("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR")), + // Gemini + new EnvironmentDetectionRuleWithResult("gemini", new BooleanEnvironmentRule("GEMINI_CLI")), + // GitHub Copilot + new EnvironmentDetectionRuleWithResult("copilot", new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE")), + // (proposed) generic flag for Agentic usage + new EnvironmentDetectionRuleWithResult("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")), ]; + /// public string? GetLLMEnvironment() { var results = _detectionRules.Select(r => r.GetResult()).Where(r => r != null).ToArray(); return results.Length > 0 ? string.Join(", ", results) : null; } -} \ No newline at end of file + + /// + public bool IsLLMEnvironment() => !string.IsNullOrEmpty(GetLLMEnvironment()); +} diff --git a/src/Cli/dotnet/xlf/CliStrings.cs.xlf b/src/Cli/dotnet/xlf/CliStrings.cs.xlf index db1fe2e0ffdc..a55342a500c5 100644 --- a/src/Cli/dotnet/xlf/CliStrings.cs.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.cs.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Spouštíte operaci instalace nástroje se zdrojem HTTP: {0}. NuGet vyžaduje zdroje HTTPS. Pokud chcete použít zdroj HTTP, musíte v souboru NuGet.Config explicitně nastavit možnost allowInsecureConnections na true. Další informace najdete na https://aka.ms/nuget-https-everywhere. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Odkaz na balíček ve formě identifikátoru balíčku, jako je Newtonsoft.Json, nebo identifikátor balíčku a verze oddělené znakem @, například Newtonsoft.Json@13.0.3. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Odkaz na balíček ve formě identifikátoru balíčku, jako je „{0}“, nebo identifikátor balíčku a verze oddělené znakem @, například „{0}@{1}“. diff --git a/src/Cli/dotnet/xlf/CliStrings.de.xlf b/src/Cli/dotnet/xlf/CliStrings.de.xlf index 0667aa4a7609..92adccab4830 100644 --- a/src/Cli/dotnet/xlf/CliStrings.de.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.de.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Sie führen den Toolinstallationsvorgang mit einer HTTP-Quelle aus: {0}. NuGet erfordert HTTPS-Quellen. Um eine HTTP-Quelle zu verwenden, müssen Sie „allowInsecureConnections“ in Ihrer NuGet.Config-Datei explizit auf TRUE festlegen. Weitere Informationen finden Sie unter https://aka.ms/nuget-https-everywhere. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Paketverweis in Form eines Paketbezeichners wie "Newtonsoft.Json" oder Paketbezeichner und -version getrennt durch "@" wie "Newtonsoft.Json@13.0.3". + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Paketverweis in Form eines Paketbezeichners wie {0} oder Paketbezeichner und -version getrennt durch „@“ wie „{0}@{1}“. diff --git a/src/Cli/dotnet/xlf/CliStrings.es.xlf b/src/Cli/dotnet/xlf/CliStrings.es.xlf index b7a48bf7ff6b..db4dd74c1447 100644 --- a/src/Cli/dotnet/xlf/CliStrings.es.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.es.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Está ejecutando la operación "tool install" con un origen "HTTP", {0}. NuGet requiere orígenes HTTPS. Para usar un origen HTTP, es necesario establecer explícitamente "allowInsecureConnections" en true en el archivo NuGet.Config. Consulte https://aka.ms/nuget-https-everywhere para obtener más información. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Referencia de paquete en forma de identificador de paquete como "Newtonsoft.Json" o identificador de paquete y versión separados por "@", como "Newtonsoft.Json@13.0.3". + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Referencia de paquete en forma de identificador de paquete como "{0}" o identificador de paquete y versión separados por "@", como "{0}@{1}". diff --git a/src/Cli/dotnet/xlf/CliStrings.fr.xlf b/src/Cli/dotnet/xlf/CliStrings.fr.xlf index 7b448a48abdc..f9b5582e4302 100644 --- a/src/Cli/dotnet/xlf/CliStrings.fr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.fr.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Vous exécutez l'opération « installation d’outils » avec une source « HTTP » : {0}. NuGet nécessite des sources HTTPS. Pour utiliser une source HTTP, vous devez définir explicitement « allowInsecureConnections » sur true dans votre fichier NuGet.Config. Reportez-vous à https://aka.ms/nuget-https-everywhere pour plus d’informations. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Référence de package sous la forme d’un identificateur de package tel que « Newtonsoft.Json » ou d’un identificateur de package et d’une version séparés par « @ », comme « Newtonsoft.Json@13.0.3 ». + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Référence de package sous la forme d’un identificateur de package tel que « {0} » ou d’un identificateur de package et d’une version séparés par « @ », comme « {0}@{1} ». diff --git a/src/Cli/dotnet/xlf/CliStrings.it.xlf b/src/Cli/dotnet/xlf/CliStrings.it.xlf index 7b9105b63c23..8e5fb8a55e03 100644 --- a/src/Cli/dotnet/xlf/CliStrings.it.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.it.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + L'operazione 'tool install' è in esecuzione con un'origine 'HTTP': {0}. NuGet richiede origini HTTPS. Per usare un’origine HTTP, è necessario impostare in modo esplicito ‘allowInsecureConnections’ su true nel file NuGet.Config. Vedere https://aka.ms/nuget-https-everywhere per altre informazioni. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Riferimento al pacchetto sotto forma di identificatore di pacchetto, ad esempio 'Newtonsoft.Json', oppure identificatore e versione di pacchetto separati da '@', ad esempio 'Newtonsoft.Json@13.0.3'. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Riferimento al pacchetto sotto forma di identificatore di pacchetto, ad esempio '{0}', oppure identificatore e versione di pacchetto separati da '@', ad esempio '{0}@{1}'. diff --git a/src/Cli/dotnet/xlf/CliStrings.ja.xlf b/src/Cli/dotnet/xlf/CliStrings.ja.xlf index fed63df4688a..2504e04d917d 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ja.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ja.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + 'tool install' 操作を、HTTP ソース {0} を使用して実行しています。NuGet には HTTPS ソースが必要です。HTTP ソースを使用するには、NuGet.Config ファイルで 'allowInsecureConnections' を true に明示的に設定する必要があります。詳しくは、https://aka.ms/nuget-https-everywhere を参照してください。 @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - 'Newtonsoft.Json' のようなパッケージ識別子の形式のパッケージ参照、または 'Newtonsoft.Json@13.0.3' のような '@' で区切られたパッケージ識別子とバージョンです。 + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + '{0}' のようなパッケージ ID の形式のパッケージ参照、または '{0}@{1}' のように '@' で区切られたパッケージ ID とバージョンです。 diff --git a/src/Cli/dotnet/xlf/CliStrings.ko.xlf b/src/Cli/dotnet/xlf/CliStrings.ko.xlf index e2c82dfd0903..af4f6dc21b68 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ko.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ko.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + 여러분은 'tool install' 작업을 'HTTP' 원본 {0}(으)로 실행하고 있습니다. NuGet에는 HTTPS 원본이 필요합니다. HTTP 원본을 사용하려면 NuGet.Config 파일에서 'allowInsecureConnections'를 명시적으로 true로 설정해야 합니다. https://aka.ms/nuget-https-everywhere에서 자세한 내용을 참조하세요. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - 'Newtonsoft.Json'과 같은 패키지 식별자 또는 'Newtonsoft.Json@13.0.3'과 같이 '@'로 구분된 패키지 식별자 및 버전 형식의 패키지 참조입니다. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + '{0}'과 같은 패키지 식별자 또는 '{0}@{1}'과 같이 '@'로 구분된 패키지 식별자 및 버전 형식의 패키지 참조입니다. diff --git a/src/Cli/dotnet/xlf/CliStrings.pl.xlf b/src/Cli/dotnet/xlf/CliStrings.pl.xlf index 04dc791d05d0..b92f633f89d0 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pl.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pl.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Operację „instalacji narzędzia” wykonujesz ze źródłem „HTTP”: {0}. Menedżer NuGet wymaga źródeł HTTPS. Aby użyć źródła HTTP, musisz wyraźnie ustawić właściwość „allowInsecureConnections” na wartość true w pliku NuGet.Config. Aby uzyskać więcej informacji, sprawdź witrynę https://aka.ms/nuget-https-everywhere. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Odwołanie do pakietu w postaci identyfikatora pakietu, takiego jak „Newtonsoft.Json” lub identyfikator pakietu i wersja rozdzielone znakiem „@”, na przykład „Newtonsoft.Json@13.0.3”. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Odwołanie do pakietu w formie identyfikatora pakietu, takiego jak „{0}” lub identyfikatora pakietu i wersji, rozdzielonych znakiem „@”, np. „{0}@{1}”. diff --git a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf index 945c6ddbef56..48cd96b70fad 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Você está executando a operação 'tool install' com uma fonte 'HTTP': {0}. O NuGet requer fontes HTTPS. Para usar uma fonte HTTP, você deve definir explicitamente 'allowInsecureConnections' como true no arquivo NuGet.Config. Consulte https://aka.ms/nuget-https-everywhere para mais informações. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Referência do pacote na forma de um identificador de pacote como 'Newtonsoft.Json' ou identificador e versão do pacote separados por '@', como 'Newtonsoft.Json\@13.0.3'. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Referência de pacote na forma de um identificador de pacote como '{0}' ou identificador de pacote e versão separados por '@', como '{0}@{1}'. diff --git a/src/Cli/dotnet/xlf/CliStrings.ru.xlf b/src/Cli/dotnet/xlf/CliStrings.ru.xlf index 8cabe6127df8..774a83ee7bd9 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ru.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ru.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + Вы выполняете операцию "установка средства" с источником "HTTP": {0}. Для NuGet требуются источники HTTPS. Чтобы использовать источник HTTP, необходимо явно задать для параметра "allowInsecureConnections" значение true в файле NuGet.Config. Дополнительные сведения см. на странице https://aka.ms/nuget-https-everywhere. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - Ссылка на пакет в виде идентификатора пакета, например "Newtonsoft.Json", или идентификатора пакета и версии, разделенных "@", например "Newtonsoft.Json@13.0.3". + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + Ссылка на пакет в виде идентификатора пакета, например "{0}", или идентификатора пакета и версии, разделенных "@", например "{0}@{1}". diff --git a/src/Cli/dotnet/xlf/CliStrings.tr.xlf b/src/Cli/dotnet/xlf/CliStrings.tr.xlf index 91803a400772..e3a3c95596ea 100644 --- a/src/Cli/dotnet/xlf/CliStrings.tr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.tr.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + 'tool install' işlemini {0} 'HTTP' kaynağıyla çalıştırıyorsunuz. NuGet için HTTPS kaynakları gereklidir. Bir HTTP kaynağı kullanmak için NuGet.Config dosyanızda 'allowInsecureConnections' ayarını açıkça true olarak ayarlamanız gerekir. Daha fazla bilgi için şuraya bakın: https://aka.ms/nuget-https-everywhere. @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - 'Newtonsoft.Json' gibi bir paket tanımlayıcısı veya 'Newtonsoft.Json@13.0.3' gibi '@' ile ayrılmış paket tanımlayıcısı ve sürümü şeklinde paket referansı. + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + '{0}' gibi bir paket tanımlayıcısı veya '{0}@{1}' gibi '@' ile ayrılmış paket tanımlayıcısı ve sürümü şeklinde paket başvurusu. diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf index 3a00f62f8ade..12a34a5c5fae 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + 正在通过 "HTTP" 源运行“工具安装”操作: {0}。NuGet 需要 HTTPS 源。要使用 HTTP 源,必须在 NuGet.Config 文件中将 "allowInsecureConnections" 显式设置为 true。有关详细信息,请参阅 https://aka.ms/nuget-https-everywhere。 @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - 包引用采用包标识符形式,如 'Newtonsoft.Json',或采用以 ‘@’ 分隔包标识符和版本的形式,如 ‘Newtonsoft.Json@13.0.3’。 + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + 包引用的格式为包标识符,如 ‘{0}’,或由 ‘@’ 分隔的包标识符和版本,如 ‘{0}@{1}’。 diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf index b6fc0105b7e7..7666715348c8 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf @@ -374,7 +374,7 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. - You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + 您正使用 'HTTP' 來源執行 'tool install' 作業: {0}。NuGet 需要 HTTPS 來源。若要使用 HTTP 來源,您必須在 NuGet.Config 檔案中將 'allowInsecureConnections' 明確設定為 true。如需詳細資訊,請參閱 https://aka.ms/nuget-https-everywhere。 @@ -783,8 +783,8 @@ setx PATH "%PATH%;{0}" - Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like 'Newtonsoft.Json@13.0.3'. - 套件參考的格式為套件識別碼,例如 'Newtonsoft.Json',或是以 '@' 分隔的套件識別碼和版本,例如 'Newtonsoft.Json@13.0.3'。 + Package reference in the form of a package identifier like '{0}' or package identifier and version separated by '@' like '{0}@{1}'. + 套件參考的格式為套件識別碼,例如 '{0}',或是以 '@' 分隔的套件識別碼和版本,例如 '{0}@{1}'。 diff --git a/src/Layout/Directory.Build.props b/src/Layout/Directory.Build.props index 479a7c0a421e..c8bc9ffafc2a 100644 --- a/src/Layout/Directory.Build.props +++ b/src/Layout/Directory.Build.props @@ -78,4 +78,10 @@ $(MSBuildThisFileDirectory)pkg\ + + <_RoslynAppHost Include="$(OutputPath)Roslyn\bincore\csc.dll" /> + <_RoslynAppHost Include="$(OutputPath)Roslyn\bincore\vbc.dll" /> + <_RoslynAppHost Include="$(OutputPath)Roslyn\bincore\VBCSCompiler.dll" /> + + diff --git a/src/Layout/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.proj b/src/Layout/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.proj index bb4234adee1d..d0a01f20c9cb 100644 --- a/src/Layout/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.proj +++ b/src/Layout/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers/VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.proj @@ -1,4 +1,4 @@ - + net472 @@ -21,6 +21,7 @@ $(ArtifactsNonShippingPackagesDir)VS.Redist.Common.Net.Core.SDK.RuntimeAnalyzers.swr + $(OutputPath)\metadata.json @@ -35,21 +36,51 @@ + - + + %(RecursiveDir) + + + %(RecursiveDir) + + + + + + + + + + + + <_VsixNamespace> + + + + + + - + + diff --git a/src/Layout/pkg/windows/bundles/sdk/bundle.wxs b/src/Layout/pkg/windows/bundles/sdk/bundle.wxs index 5836502fcd6c..2503b1dac02e 100644 --- a/src/Layout/pkg/windows/bundles/sdk/bundle.wxs +++ b/src/Layout/pkg/windows/bundles/sdk/bundle.wxs @@ -27,7 +27,13 @@ - + + + + + + diff --git a/src/Layout/redist/dnx.ps1 b/src/Layout/redist/dnx.ps1 deleted file mode 100644 index ff880f85e2f6..000000000000 --- a/src/Layout/redist/dnx.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# Licensed to the .NET Foundation under one or more agreements. -# The .NET Foundation licenses this file to you under the MIT license. - -# PowerShell script to launch dotnet.exe with 'dnx' and all passed arguments -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$dotnet = Join-Path $scriptDir 'dotnet.exe' -& $dotnet dnx @Args -exit $LASTEXITCODE diff --git a/src/Layout/redist/redist.csproj b/src/Layout/redist/redist.csproj index 803a0334f542..22d3827eab44 100644 --- a/src/Layout/redist/redist.csproj +++ b/src/Layout/redist/redist.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/Layout/redist/targets/Crossgen.targets b/src/Layout/redist/targets/Crossgen.targets index 909c5de0bf3c..3e07f481af11 100644 --- a/src/Layout/redist/targets/Crossgen.targets +++ b/src/Layout/redist/targets/Crossgen.targets @@ -197,6 +197,7 @@ + diff --git a/src/Layout/redist/targets/GenerateInstallerLayout.targets b/src/Layout/redist/targets/GenerateInstallerLayout.targets index 4c7aa7749e3b..069c6a2fde2a 100644 --- a/src/Layout/redist/targets/GenerateInstallerLayout.targets +++ b/src/Layout/redist/targets/GenerateInstallerLayout.targets @@ -67,7 +67,6 @@ - diff --git a/src/Layout/redist/targets/GenerateLayout.targets b/src/Layout/redist/targets/GenerateLayout.targets index 65d4aff197c6..0b5bcb6fa182 100644 --- a/src/Layout/redist/targets/GenerateLayout.targets +++ b/src/Layout/redist/targets/GenerateLayout.targets @@ -59,6 +59,12 @@ + + @@ -495,6 +501,7 @@ + diff --git a/src/Layout/redist/targets/GeneratePackagePruneData.targets b/src/Layout/redist/targets/GeneratePackagePruneData.targets index 157eeabd5cd4..f414f6582c75 100644 --- a/src/Layout/redist/targets/GeneratePackagePruneData.targets +++ b/src/Layout/redist/targets/GeneratePackagePruneData.targets @@ -30,13 +30,12 @@ - - + - + DependsOnTargets="GetTargetingPacksForPruneData"> + diff --git a/src/Layout/redist/targets/OverlaySdkOnLKG.targets b/src/Layout/redist/targets/OverlaySdkOnLKG.targets index 19505de69d50..faf995466d4f 100644 --- a/src/Layout/redist/targets/OverlaySdkOnLKG.targets +++ b/src/Layout/redist/targets/OverlaySdkOnLKG.targets @@ -34,7 +34,7 @@ + UseHardLinksIfPossible="false" /> + diff --git a/src/Microsoft.Net.Sdk.AnalyzerRedirecting/SdkAnalyzerAssemblyRedirector.cs b/src/Microsoft.Net.Sdk.AnalyzerRedirecting/SdkAnalyzerAssemblyRedirector.cs index c81a829990c2..23736deeb43e 100644 --- a/src/Microsoft.Net.Sdk.AnalyzerRedirecting/SdkAnalyzerAssemblyRedirector.cs +++ b/src/Microsoft.Net.Sdk.AnalyzerRedirecting/SdkAnalyzerAssemblyRedirector.cs @@ -3,7 +3,10 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; +using System.Text.Json; using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; // Example: // FullPath: "C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll" @@ -13,9 +16,16 @@ namespace Microsoft.Net.Sdk.AnalyzerRedirecting; +/// +/// See documentation/general/analyzer-redirecting.md. +/// [Export(typeof(IAnalyzerAssemblyRedirector))] public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector { + private readonly IVsActivityLog? _log; + + private readonly bool _enabled; + private readonly string? _insertedAnalyzersDirectory; /// @@ -24,62 +34,91 @@ public sealed class SdkAnalyzerAssemblyRedirector : IAnalyzerAssemblyRedirector private readonly ImmutableDictionary> _analyzerMap; [ImportingConstructor] - public SdkAnalyzerAssemblyRedirector() - : this(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\DotNetRuntimeAnalyzers"))) { } + public SdkAnalyzerAssemblyRedirector(SVsServiceProvider serviceProvider) : this( + Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"CommonExtensions\Microsoft\DotNet")), + serviceProvider.GetService()) + { + } // Internal for testing. - internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory) + internal SdkAnalyzerAssemblyRedirector(string? insertedAnalyzersDirectory, IVsActivityLog? log = null) { + _log = log; + var enable = Environment.GetEnvironmentVariable("DOTNET_ANALYZER_REDIRECTING"); + _enabled = !"0".Equals(enable, StringComparison.OrdinalIgnoreCase) && !"false".Equals(enable, StringComparison.OrdinalIgnoreCase); _insertedAnalyzersDirectory = insertedAnalyzersDirectory; _analyzerMap = CreateAnalyzerMap(); } private ImmutableDictionary> CreateAnalyzerMap() { + if (!_enabled) + { + Log("Analyzer redirecting is disabled."); + return ImmutableDictionary>.Empty; + } + + var metadataFilePath = Path.Combine(_insertedAnalyzersDirectory, "metadata.json"); + if (!File.Exists(metadataFilePath)) + { + Log($"File does not exist: {metadataFilePath}"); + return ImmutableDictionary>.Empty; + } + + var versions = JsonSerializer.Deserialize>(File.ReadAllText(metadataFilePath)); + if (versions is null || versions.Count == 0) + { + Log($"Versions are empty: {metadataFilePath}"); + return ImmutableDictionary>.Empty; + } + var builder = ImmutableDictionary.CreateBuilder>(StringComparer.OrdinalIgnoreCase); // Expects layout like: - // VsInstallDir\SDK\RuntimeAnalyzers\WindowsDesktopAnalyzers\8.0.8\analyzers\dotnet\System.Windows.Forms.Analyzers.dll - // ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory - // ~~~~~ = versionDirectory - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath + // VsInstallDir\DotNetRuntimeAnalyzers\WindowsDesktopAnalyzers\analyzers\dotnet\System.Windows.Forms.Analyzers.dll + // ~~~~~~~~~~~~~~~~~~~~~~~ = topLevelDirectory + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ = analyzerPath foreach (string topLevelDirectory in Directory.EnumerateDirectories(_insertedAnalyzersDirectory)) { - foreach (string versionDirectory in Directory.EnumerateDirectories(topLevelDirectory)) + foreach (string analyzerPath in Directory.EnumerateFiles(topLevelDirectory, "*.dll", SearchOption.AllDirectories)) { - foreach (string analyzerPath in Directory.EnumerateFiles(versionDirectory, "*.dll", SearchOption.AllDirectories)) + if (!analyzerPath.StartsWith(topLevelDirectory, StringComparison.OrdinalIgnoreCase)) { - if (!analyzerPath.StartsWith(versionDirectory, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - string version = Path.GetFileName(versionDirectory); - string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath); - string pathSuffix = analyzerPath.Substring(versionDirectory.Length + 1 /* slash */); - pathSuffix = Path.GetDirectoryName(pathSuffix); - - AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix }; - - if (builder.TryGetValue(analyzerName, out var existing)) - { - existing.Add(analyzer); - } - else - { - builder.Add(analyzerName, [analyzer]); - } + continue; + } + + string subsetName = Path.GetFileName(topLevelDirectory); + if (!versions.TryGetValue(subsetName, out string version)) + { + continue; + } + + string analyzerName = Path.GetFileNameWithoutExtension(analyzerPath); + string pathSuffix = analyzerPath.Substring(topLevelDirectory.Length + 1 /* slash */); + pathSuffix = Path.GetDirectoryName(pathSuffix); + + AnalyzerInfo analyzer = new() { FullPath = analyzerPath, ProductVersion = version, PathSuffix = pathSuffix }; + + if (builder.TryGetValue(analyzerName, out var existing)) + { + existing.Add(analyzer); + } + else + { + builder.Add(analyzerName, [analyzer]); } } } + Log($"Loaded analyzer map ({builder.Count}): {_insertedAnalyzersDirectory}"); + return builder.ToImmutable(); } public string? RedirectPath(string fullPath) { - if (_analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) + if (_enabled && _analyzerMap.TryGetValue(Path.GetFileNameWithoutExtension(fullPath), out var analyzers)) { foreach (AnalyzerInfo analyzer in analyzers) { @@ -134,4 +173,12 @@ static bool areVersionMajorMinorPartEqual(string version1, string version2) return 0 == string.Compare(version1, 0, version2, 0, secondDotIndex, StringComparison.OrdinalIgnoreCase); } } + + private void Log(string message) + { + _log?.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, + nameof(SdkAnalyzerAssemblyRedirector), + message); + } } diff --git a/src/Microsoft.Net.Sdk.AnalyzerRedirecting/source.extension.vsixmanifest b/src/Microsoft.Net.Sdk.AnalyzerRedirecting/source.extension.vsixmanifest index 3c3150875aab..cfa767fb3ca0 100644 --- a/src/Microsoft.Net.Sdk.AnalyzerRedirecting/source.extension.vsixmanifest +++ b/src/Microsoft.Net.Sdk.AnalyzerRedirecting/source.extension.vsixmanifest @@ -13,10 +13,10 @@ - + - - + + amd64 diff --git a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs index 6b891c586a23..ae38c8e2f61c 100644 --- a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs +++ b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -32,7 +32,7 @@ public sealed class DotNetMSBuildSdkResolver : SdkResolver private readonly Func _getMsbuildRuntime; private readonly NETCoreSdkResolver _netCoreSdkResolver; - private const string DOTNET_HOST = nameof(DOTNET_HOST); + private const string DOTNET_HOST_PATH = nameof(DOTNET_HOST_PATH); private const string DotnetHostExperimentalKey = "DOTNET_EXPERIMENTAL_HOST_PATH"; private const string MSBuildTaskHostRuntimeVersion = "SdkResolverMSBuildTaskHostRuntimeVersion"; private const string SdkResolverHonoredGlobalJson = "SdkResolverHonoredGlobalJson"; @@ -245,12 +245,12 @@ private sealed class CachedState // this is the future-facing implementation. environmentVariablesToAdd ??= new Dictionary(1) { - [DOTNET_HOST] = fullPathToMuxer + [DOTNET_HOST_PATH] = fullPathToMuxer }; } else { - logger?.LogMessage($"Could not set '{DOTNET_HOST}' environment variable because dotnet executable '{fullPathToMuxer}' does not exist."); + logger?.LogMessage($"Could not set '{DOTNET_HOST_PATH}' environment variable because dotnet executable '{fullPathToMuxer}' does not exist."); } string? runtimeVersion = dotnetRoot != null ? diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs index 678577e9053f..a6d9f6d808ae 100644 --- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs +++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs @@ -28,7 +28,7 @@ static Interop() } // MSBuild SDK resolvers are required to be AnyCPU, but we have a native dependency and .NETFramework does not - // have a built-in facility for dynamically loading user native dlls for the appropriate platform. We therefore + // have a built-in facility for dynamically loading user native dlls for the appropriate platform. We therefore // preload the version with the correct architecture (from a corresponding sub-folder relative to us) on static // construction so that subsequent P/Invokes can find it. private static void PreloadWindowsLibrary(string dllFileName) @@ -124,6 +124,12 @@ internal static extern int hostfxr_get_dotnet_environment_info( hostfxr_get_dotnet_environment_info_result_fn result, IntPtr result_context); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void hostfxr_error_writer_fn(IntPtr message); + + [DllImport(Constants.HostFxr, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr hostfxr_set_error_writer(IntPtr error_writer); + public static class Windows { private const CharSet UTF16 = CharSet.Unicode; diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/NETCoreSdkResolverNativeWrapper.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/NETCoreSdkResolverNativeWrapper.cs index 160346fbccfc..b0571d439d09 100644 --- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/NETCoreSdkResolverNativeWrapper.cs +++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/NETCoreSdkResolverNativeWrapper.cs @@ -23,6 +23,26 @@ public static SdkResolutionResult ResolveSdk( return result; } + public static string? GetGlobalJsonState(string globalJsonStartDirectory) + { + // We don't care about the actual SDK resolution, just the global.json information, + // so just pass empty string as executable directory for resolution. This means that + // we expect the call to fail to resolve an SDK. Set up the error writer to avoid + // output going to stderr. We reset it after the call. + var swallowErrors = new Interop.hostfxr_error_writer_fn(message => { }); + IntPtr errorWriter = Marshal.GetFunctionPointerForDelegate(swallowErrors); + IntPtr previousErrorWriter = Interop.hostfxr_set_error_writer(errorWriter); + try + { + SdkResolutionResult result = ResolveSdk(string.Empty, globalJsonStartDirectory); + return result.GlobalJsonState; + } + finally + { + Interop.hostfxr_set_error_writer(previousErrorWriter); + } + } + private sealed class SdkList { public string[]? Entries; diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs index 0387bb131057..f996a75b7168 100644 --- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs +++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs @@ -116,6 +116,8 @@ public override bool Execute() endpoint.AssetFile = asset.ResolvedAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); endpoint.Route = route; + EncodeLinkHeadersIfNeeded(endpoint); + Log.LogMessage(MessageImportance.Low, "Including endpoint '{0}' for asset '{1}' with final location '{2}'", endpoint.Route, endpoint.AssetFile, asset.TargetPath); } @@ -137,6 +139,48 @@ public override bool Execute() return !Log.HasLoggedErrors; } + private static void EncodeLinkHeadersIfNeeded(StaticWebAssetEndpoint endpoint) + { + for (var i = 0; i < endpoint.ResponseHeaders.Length; i++) + { + ref var header = ref endpoint.ResponseHeaders[i]; + if (!string.Equals(header.Name, "Link", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + var headerValues = header.Value.Split([','], StringSplitOptions.RemoveEmptyEntries); + for (var j = 0; j < headerValues.Length; j++) + { + ref var value = ref headerValues[j]; + value = EncodeHeaderValue(value); + } + header.Value = string.Join(",", headerValues); + } + } + + private static string EncodeHeaderValue(string header) + { + var index = header.IndexOf('<'); + if (index == -1) + { + return header; + } + index++; + var endIndex = header.IndexOf('>', index); + if (endIndex == -1) + { + return header; + } + var link = header.AsSpan(index, endIndex - index).ToString(); + var segments = link.Split('/'); + for (var j = 0; j < segments.Length; j++) + { + segments[j] = System.Net.WebUtility.UrlEncode(segments[j]); + } + var encoded = string.Join("/", segments); + return $"{header.Substring(0, index)}{encoded}{header.Substring(endIndex)}"; + } + private static (string, string[]) ParseAndSortPatterns(string patterns) { if (string.IsNullOrEmpty(patterns)) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index acb871529a97..7b5b7bc0da15 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -817,20 +817,36 @@ private ToolPackSupport AddToolPack( { var packNamePattern = knownPack.GetMetadata(packName + "PackNamePattern"); var packSupportedRuntimeIdentifiers = knownPack.GetMetadata(packName + "RuntimeIdentifiers").Split(';'); - // When publishing for the non-portable RID that matches NETCoreSdkRuntimeIdentifier, prefer NETCoreSdkRuntimeIdentifier for the host. + var packSupportedPortableRuntimeIdentifiers = knownPack.GetMetadata(packName + "PortableRuntimeIdentifiers").Split(';'); + + // When publishing for a non-portable RID, prefer NETCoreSdkRuntimeIdentifier for the host. // Otherwise prefer the NETCoreSdkPortableRuntimeIdentifier. - // This makes non-portable SDKs behave the same as portable SDKs except for the specific case of targetting the non-portable RID. - // It also enables the non-portable ILCompiler to be packaged separately from the SDK and - // only required when publishing for the non-portable SDK RID. - string portableSdkRid = !string.IsNullOrEmpty(NETCoreSdkPortableRuntimeIdentifier) ? NETCoreSdkPortableRuntimeIdentifier : NETCoreSdkRuntimeIdentifier; - bool targetsNonPortableSdkRid = EffectiveRuntimeIdentifier == NETCoreSdkRuntimeIdentifier && NETCoreSdkRuntimeIdentifier != portableSdkRid; - string? hostRuntimeIdentifier = targetsNonPortableSdkRid ? NETCoreSdkRuntimeIdentifier : portableSdkRid; - Log.LogMessage(MessageImportance.Low, $"Determining best RID for '{knownPack.ItemSpec}@{packVersion}' for '{hostRuntimeIdentifier}' from among '{knownPack.GetMetadata(packName + "RuntimeIdentifiers")}'"); - // Get the best RID for the host machine, which will be used to validate that we can run crossgen for the target platform and architecture + // This makes non-portable SDKs behave the same as portable SDKs except for the specific case of targetting a non-portable RID. + // This ensures that targeting portable RIDs doesn't require any non-portable assets that aren't packaged in the SDK. + // Due to size concerns, the non-portable ILCompiler and Crossgen2 aren't included by default in non-portable SDK distributions. var runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(RuntimeGraphPath); - hostRuntimeIdentifier = NuGetUtils.GetBestMatchingRid(runtimeGraph, hostRuntimeIdentifier, packSupportedRuntimeIdentifiers, out bool wasInGraph); + + // Prefer portable when the "supported RID" for the tool pack is the same RID as the "supported portable RID". + // This makes non-portable SDKs behave the same as portable SDKs except for the specific cases added to "supported", such as targeting the non-portable RID. + // This also ensures that targeting common RIDs doesn't require any non-portable assets that aren't packaged in the SDK by default. + // Due to size concerns, the non-portable ILCompiler and Crossgen2 aren't included by default in non-portable SDK distributions. + var runtimeIdentifier = RuntimeIdentifier ?? "any"; + string? supportedTargetRid = NuGetUtils.GetBestMatchingRid(runtimeGraph, runtimeIdentifier, packSupportedRuntimeIdentifiers, out _); + string? supportedPortableTargetRid = NuGetUtils.GetBestMatchingRid(runtimeGraph, runtimeIdentifier, packSupportedPortableRuntimeIdentifiers, out _); + + bool usePortable = !string.IsNullOrEmpty(NETCoreSdkPortableRuntimeIdentifier) + && supportedTargetRid is not null && supportedPortableTargetRid is not null + && supportedTargetRid == supportedPortableTargetRid; + + // Get the best RID for the host machine, which will be used to validate that we can run crossgen for the target platform and architecture + Log.LogMessage(MessageImportance.Low, $"Determining best RID for '{knownPack.ItemSpec}@{packVersion}' from among '{knownPack.GetMetadata(packName + "RuntimeIdentifiers")}'"); + string? hostRuntimeIdentifier = usePortable + ? NuGetUtils.GetBestMatchingRid(runtimeGraph, NETCoreSdkPortableRuntimeIdentifier!, packSupportedPortableRuntimeIdentifiers, out _) + : NuGetUtils.GetBestMatchingRid(runtimeGraph, NETCoreSdkRuntimeIdentifier!, packSupportedRuntimeIdentifiers, out _); + if (hostRuntimeIdentifier == null) { + Log.LogMessage(MessageImportance.Low, $"No matching RID was found'"); return ToolPackSupport.UnsupportedForHostRuntimeIdentifier; } Log.LogMessage(MessageImportance.Low, $"Best RID for '{knownPack.ItemSpec}@{packVersion}' is '{hostRuntimeIdentifier}'"); diff --git a/src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs b/src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs index f899342e578e..fd5d0e6370ba 100644 --- a/src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs +++ b/src/Tasks/sdk-tasks/GenerateRuntimeAnalyzersSWR.cs @@ -19,11 +19,19 @@ public override bool Execute() // NOTE: Keep in sync with SdkAnalyzerAssemblyRedirector. // This is intentionally short to avoid long path problems. - const string installDir = @"DotNetRuntimeAnalyzers"; + const string installDir = @"Common7\IDE\CommonExtensions\Microsoft\DotNet"; AddFolder(sb, - @"AnalyzerRedirecting", - @"Common7\IDE\CommonExtensions\Microsoft\AnalyzerRedirecting", + "", + installDir, + filesToInclude: + [ + "metadata.json", + ]); + + AddFolder(sb, + "AnalyzerRedirecting", + @$"{installDir}\AnalyzerRedirecting", filesToInclude: [ "Microsoft.Net.Sdk.AnalyzerRedirecting.dll", @@ -32,23 +40,23 @@ public override bool Execute() ]); AddFolder(sb, - @"AspNetCoreAnalyzers", + "AspNetCoreAnalyzers", @$"{installDir}\AspNetCoreAnalyzers"); AddFolder(sb, - @"NetCoreAnalyzers", + "NetCoreAnalyzers", @$"{installDir}\NetCoreAnalyzers"); AddFolder(sb, - @"WindowsDesktopAnalyzers", + "WindowsDesktopAnalyzers", @$"{installDir}\WindowsDesktopAnalyzers"); AddFolder(sb, - @"SDKAnalyzers", + "SDKAnalyzers", @$"{installDir}\SDKAnalyzers"); AddFolder(sb, - @"WebSDKAnalyzers", + "WebSDKAnalyzers", @$"{installDir}\WebSDKAnalyzers"); File.WriteAllText(OutputFile, sb.ToString()); diff --git a/src/Tasks/sdk-tasks/ProcessRuntimeAnalyzerVersions.cs b/src/Tasks/sdk-tasks/ProcessRuntimeAnalyzerVersions.cs new file mode 100644 index 000000000000..e1f7df9dd671 --- /dev/null +++ b/src/Tasks/sdk-tasks/ProcessRuntimeAnalyzerVersions.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.DotNet.Build.Tasks; + +/// +/// Extracts version numbers and saves them into a metadata file. +/// +public sealed class ProcessRuntimeAnalyzerVersions : Task +{ + [Required] + public ITaskItem[]? Inputs { get; set; } + + [Required] + public string? MetadataFilePath { get; set; } + + [Output] + public ITaskItem[]? Outputs { get; set; } + + public override bool Execute() + { + var metadata = new Dictionary(); + + // Extract version from a path like: + // ...\packs\Microsoft.NetCore.App.Ref\\analyzers\**\*.* + // The version segment is always the first segment in %(RecursiveDir). + foreach (var input in Inputs ?? []) + { + var deploymentSubpath = input.GetMetadata("DeploymentSubpath"); + var recursiveDir = input.GetMetadata("CustomRecursiveDir"); + + var slashIndex = recursiveDir.IndexOfAny('/', '\\'); + var version = recursiveDir.Substring(0, slashIndex); + var rest = recursiveDir.Substring(slashIndex + 1); + + input.SetMetadata("CustomRecursiveDir", rest); + + if (!metadata.TryGetValue(deploymentSubpath, out var existingVersion)) + { + metadata.Add(deploymentSubpath, version); + } + else if (existingVersion != version) + { + Log.LogError($"Version mismatch for '{deploymentSubpath}': '{existingVersion}' != '{version}'. " + + $"Expected only one version of '{input.GetMetadata("Identity")}'."); + return false; + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(MetadataFilePath)!); + File.WriteAllText(path: MetadataFilePath!, JsonSerializer.Serialize(metadata)); + + Outputs = Inputs; + return true; + } +} diff --git a/src/Tasks/sdk-tasks/sdk-tasks.InTree.targets b/src/Tasks/sdk-tasks/sdk-tasks.InTree.targets index 6f22d01dfbbd..547f8ccd7595 100644 --- a/src/Tasks/sdk-tasks/sdk-tasks.InTree.targets +++ b/src/Tasks/sdk-tasks/sdk-tasks.InTree.targets @@ -13,7 +13,6 @@ - @@ -28,6 +27,7 @@ + diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/EditorConfig/Dotnet/.editorconfig b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/EditorConfig/Dotnet/.editorconfig index bb88786e7b54..af3d70051e21 100644 --- a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/EditorConfig/Dotnet/.editorconfig +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/EditorConfig/Dotnet/.editorconfig @@ -9,7 +9,7 @@ indent_style = space indent_size = 2 # Xml project files -[*.{csproj,fsproj,vbproj,proj}] +[*.{csproj,fsproj,vbproj,proj,slnx}] indent_size = 2 # Xml config files diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/.template.config/template.json index 01977ec20d99..642d2f3b5a19 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/.template.config/template.json @@ -240,7 +240,9 @@ "parentPropertyPath": "test", "newJsonPropertyName": "runner", "newJsonPropertyValue": "Microsoft.Testing.Platform", - "detectRepositoryRootForFileCreation": true + "detectRepositoryRoot": true, + "includeAllDirectoriesInSearch": false, + "includeAllParentDirectoriesInSearch": true }, "continueOnError": true }, diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/Company.TestProject1.csproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/Company.TestProject1.csproj index b4490a1d30fe..415f0f4b220a 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/Company.TestProject1.csproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-CSharp/Company.TestProject1.csproj @@ -1,5 +1,5 @@  - + net10.0 @@ -43,7 +43,7 @@ - + diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/.template.config/template.json index 022ffe70b9cf..ed388b0b28da 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/.template.config/template.json @@ -240,7 +240,9 @@ "parentPropertyPath": "test", "newJsonPropertyName": "runner", "newJsonPropertyValue": "Microsoft.Testing.Platform", - "detectRepositoryRootForFileCreation": true + "detectRepositoryRoot": true, + "includeAllDirectoriesInSearch": false, + "includeAllParentDirectoriesInSearch": true }, "continueOnError": true }, diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/Company.TestProject1.fsproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/Company.TestProject1.fsproj index a1b85e78555b..6c64e1cd1165 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/Company.TestProject1.fsproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-FSharp/Company.TestProject1.fsproj @@ -1,5 +1,5 @@ - + net10.0 @@ -48,7 +48,7 @@ - + diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/.template.config/template.json index cca88e73c3cd..a110969e6973 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/.template.config/template.json @@ -240,7 +240,9 @@ "parentPropertyPath": "test", "newJsonPropertyName": "runner", "newJsonPropertyValue": "Microsoft.Testing.Platform", - "detectRepositoryRootForFileCreation": true + "detectRepositoryRoot": true, + "includeAllDirectoriesInSearch": false, + "includeAllParentDirectoriesInSearch": true }, "continueOnError": true }, diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/Company.TestProject1.vbproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/Company.TestProject1.vbproj index b4490a1d30fe..415f0f4b220a 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/Company.TestProject1.vbproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/MSTest-VisualBasic/Company.TestProject1.vbproj @@ -1,5 +1,5 @@  - + net10.0 @@ -43,7 +43,7 @@ - + diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/.template.config/template.json index b55dff5e27a9..cca0c7262957 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/.template.config/template.json @@ -260,7 +260,9 @@ "parentPropertyPath": "test", "newJsonPropertyName": "runner", "newJsonPropertyValue": "Microsoft.Testing.Platform", - "detectRepositoryRootForFileCreation": true + "detectRepositoryRoot": true, + "includeAllDirectoriesInSearch": false, + "includeAllParentDirectoriesInSearch": true }, "continueOnError": true }, diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/Company.TestProject1.csproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/Company.TestProject1.csproj index 9c6ca5d3e253..2cffa453be01 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/Company.TestProject1.csproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.10.0/content/Playwright-MSTest-CSharp/Company.TestProject1.csproj @@ -1,5 +1,5 @@  - + net10.0 @@ -44,8 +44,8 @@ - - + + diff --git a/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs index ba14cd54eebc..2f473b12d72f 100644 --- a/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs +++ b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs @@ -6,6 +6,7 @@ using Basic.CompilerLog.Util; using Microsoft.Build.Logging.StructuredLogger; using Microsoft.CodeAnalysis; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.NET.Build.Tests; @@ -18,16 +19,16 @@ public sealed class RoslynBuildTaskTests(ITestOutputHelper log) : SdkTest(log) _ => throw new ArgumentOutOfRangeException(paramName: nameof(language)), }; - private static string CoreCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + ".dll"; + private static string DotNetExecCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + ".dll"; - private static string FxCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + ".exe"; + private static string AppHostCompilerFileName(Language language) => CompilerFileNameWithoutExtension(language) + FileNameSuffixes.CurrentPlatform.Exe; [FullMSBuildOnlyTheory, CombinatorialData] public void FullMSBuild_SdkStyle(bool useSharedCompilation, Language language) { var testAsset = CreateProject(useSharedCompilation, language); var buildCommand = BuildAndRunUsingMSBuild(testAsset); - VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation); + VerifyCompiler(buildCommand, AppHostCompilerFileName(language), useSharedCompilation); } [FullMSBuildOnlyTheory, CombinatorialData] @@ -38,7 +39,7 @@ public void FullMSBuild_SdkStyle_OptOut(bool useSharedCompilation, Language lang doc.Root!.Element("PropertyGroup")!.Add(new XElement("RoslynCompilerType", "Framework")); }); var buildCommand = BuildAndRunUsingMSBuild(testAsset); - VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation); + VerifyCompiler(buildCommand, AppHostCompilerFileName(language), useSharedCompilation); } [FullMSBuildOnlyTheory, CombinatorialData] @@ -50,7 +51,7 @@ public void FullMSBuild_NonSdkStyle(bool useSharedCompilation, Language language project.TargetFrameworkVersion = "v4.7.2"; }); var buildCommand = BuildAndRunUsingMSBuild(testAsset); - VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation); + VerifyCompiler(buildCommand, AppHostCompilerFileName(language), useSharedCompilation); } [FullMSBuildOnlyTheory, CombinatorialData] @@ -58,7 +59,7 @@ public void FullMSBuild_SdkStyle_ToolsetPackage(bool useSharedCompilation, Langu { var testAsset = CreateProject(useSharedCompilation, language, AddCompilersToolsetPackage); var buildCommand = BuildAndRunUsingMSBuild(testAsset); - VerifyCompiler(buildCommand, FxCompilerFileName(language), useSharedCompilation, toolsetPackage: true); + VerifyCompiler(buildCommand, AppHostCompilerFileName(language), useSharedCompilation, toolsetPackage: true); } [Theory, CombinatorialData] @@ -66,7 +67,7 @@ public void DotNet(bool useSharedCompilation, Language language) { var testAsset = CreateProject(useSharedCompilation, language); var buildCommand = BuildAndRunUsingDotNet(testAsset); - VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation); + VerifyCompiler(buildCommand, AppHostCompilerFileName(language), useSharedCompilation); } // https://github.com/dotnet/sdk/issues/49665 @@ -75,7 +76,7 @@ public void DotNet_ToolsetPackage(bool useSharedCompilation, Language language) { var testAsset = CreateProject(useSharedCompilation, language, AddCompilersToolsetPackage); var buildCommand = BuildAndRunUsingDotNet(testAsset); - VerifyCompiler(buildCommand, CoreCompilerFileName(language), useSharedCompilation, toolsetPackage: true); + VerifyCompiler(buildCommand, DotNetExecCompilerFileName(language), useSharedCompilation, toolsetPackage: true); } private TestAsset CreateProject(bool useSharedCompilation, Language language, Action? configure = null, [CallerMemberName] string callingMethod = "") diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs index a4cfbcb6d1f6..20afab0e873f 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs @@ -546,7 +546,8 @@ private void OverrideKnownILCompilerPackRuntimeIdentifiers(XDocument project, st project.Root.Add(new XElement(ns + "ItemGroup", new XElement(ns + "KnownILCompilerPack", new XAttribute("Update", "@(KnownILCompilerPack)"), - new XElement(ns + "ILCompilerRuntimeIdentifiers", runtimeIdentifiers)))); + new XElement(ns + "ILCompilerRuntimeIdentifiers", runtimeIdentifiers), + new XElement(ns + "ILCompilerPortableRuntimeIdentifiers", runtimeIdentifiers)))); } [RequiresMSBuildVersionTheory("17.0.0.32901")] diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishIncrementally.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishIncrementally.cs index c47c60089692..9cd038e21ff5 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishIncrementally.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishIncrementally.cs @@ -134,7 +134,7 @@ public void It_cleans_between_single_file_publishes() CheckPublishOutput(publishDir, expectedSingleExeFiles.Append(testProject.Name + ".dll"), null); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/sdk/issues/50784")] public void It_cleans_before_trimmed_single_file_publish() { var testProject = new TestProject() diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs index 9668d8656913..6155a3498987 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.AspNetCore.StaticWebAssets.Tasks; @@ -617,5 +618,72 @@ public void RegeneratingScopedCss_ForProjectWithReferences() text.Should().Contain("background-color: orangered"); text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); } + + [Fact] + public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName() + { + var testAsset = "RazorAppWithPackageAndP2PReference"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + // Rename the ClassLibrary project to have non-ASCII characters + var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib"); + var newLibPath = Path.Combine(ProjectDirectory.Path, "项目"); + Directory.Move(originalLibPath, newLibPath); + + // Update the project file to set the assembly name and package ID + var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj"); + var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj"); + File.Move(libProjectFile, newLibProjectFile); + + // Add assembly name property to ensure consistent naming + var libProjectContent = File.ReadAllText(newLibProjectFile); + // Find the first PropertyGroup closing tag and replace it + var targetPattern = ""; + var replacement = " 项目\n 项目\n "; + var index = libProjectContent.IndexOf(targetPattern); + if (index >= 0) + { + libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length); + } + File.WriteAllText(newLibProjectFile, libProjectContent); + + // Update the main project to reference the renamed library + var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj"); + var mainProjectContent = File.ReadAllText(mainProjectFile); + mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj"); + File.WriteAllText(mainProjectFile, mainProjectContent); + + // Ensure library has scoped CSS + var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css"); + if (!File.Exists(libCssFile)) + { + Directory.CreateDirectory(Path.GetDirectoryName(libCssFile)); + File.WriteAllText(libCssFile, ".test { color: red; }"); + } + + EnsureLocalPackagesExists(); + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + ExecuteCommand(restore).Should().Pass(); + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + ExecuteCommand(build).Should().Pass(); + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + // Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters + var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); + new FileInfo(endpointsFile).Should().Exist(); + + var endpointsContent = File.ReadAllText(endpointsFile); + var json = JsonSerializer.Deserialize(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css")); + + foreach (var styleEndpoint in styles) + { + styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE")); + } + } } } diff --git a/test/Microsoft.Net.Sdk.AnalyzerRedirecting.Tests/SdkAnalyzerAssemblyRedirectorTests.cs b/test/Microsoft.Net.Sdk.AnalyzerRedirecting.Tests/SdkAnalyzerAssemblyRedirectorTests.cs index 62a042d8f419..956161b134a8 100644 --- a/test/Microsoft.Net.Sdk.AnalyzerRedirecting.Tests/SdkAnalyzerAssemblyRedirectorTests.cs +++ b/test/Microsoft.Net.Sdk.AnalyzerRedirecting.Tests/SdkAnalyzerAssemblyRedirectorTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; + namespace Microsoft.Net.Sdk.AnalyzerRedirecting.Tests; public class SdkAnalyzerAssemblyRedirectorTests(ITestOutputHelper log) : SdkTest(log) @@ -16,7 +18,8 @@ public void SameMajorMinorVersion(string a, string b) TestDirectory testDir = _testAssetsManager.CreateTestDirectory(identifier: "RuntimeAnalyzers"); var vsDir = Path.Combine(testDir.Path, "vs"); - var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\{a}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + var vsAnalyzerPath = FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); @@ -30,7 +33,8 @@ public void DifferentPathSuffix() TestDirectory testDir = _testAssetsManager.CreateTestDirectory(identifier: "RuntimeAnalyzers"); var vsDir = Path.Combine(testDir.Path, "vs"); - FakeDll(vsDir, @"AspNetCoreAnalyzers\9.0.0-preview.5.24306.11\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", "9.0.0-preview.5.24306.11" } }); + FakeDll(vsDir, @"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @"sdk\packs\Microsoft.AspNetCore.App.Ref\9.0.0-preview.7.24406.2\analyzers\dotnet\vb", "Microsoft.AspNetCore.App.Analyzers"); var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); @@ -50,7 +54,8 @@ public void DifferentMajorMinorVersion(string a, string b) TestDirectory testDir = _testAssetsManager.CreateTestDirectory(identifier: "RuntimeAnalyzers"); var vsDir = Path.Combine(testDir.Path, "vs"); - FakeDll(vsDir, @$"AspNetCoreAnalyzers\{a}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); + Metadata(vsDir, new() { { "AspNetCoreAnalyzers", a } }); + FakeDll(vsDir, @$"AspNetCoreAnalyzers\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var sdkAnalyzerPath = FakeDll(testDir.Path, @$"sdk\packs\Microsoft.AspNetCore.App.Ref\{b}\analyzers\dotnet\cs", "Microsoft.AspNetCore.App.Analyzers"); var resolver = new SdkAnalyzerAssemblyRedirector(vsDir); @@ -65,4 +70,11 @@ private static string FakeDll(string root, string subdir, string name) File.WriteAllText(dllPath, ""); return dllPath; } + + private static void Metadata(string root, Dictionary versions) + { + var metadataFilePath = Path.Combine(root, "metadata.json"); + Directory.CreateDirectory(Path.GetDirectoryName(metadataFilePath)); + File.WriteAllText(metadataFilePath, JsonSerializer.Serialize(versions)); + } } diff --git a/test/TestAssets/TestProjects/TestProjectSolutionWithCodeCoverage/TestProject/TestProject.csproj b/test/TestAssets/TestProjects/TestProjectSolutionWithCodeCoverage/TestProject/TestProject.csproj index ff9709d80d68..7bf348d2d585 100644 --- a/test/TestAssets/TestProjects/TestProjectSolutionWithCodeCoverage/TestProject/TestProject.csproj +++ b/test/TestAssets/TestProjects/TestProjectSolutionWithCodeCoverage/TestProject/TestProject.csproj @@ -1,13 +1,12 @@ - + $(CurrentTargetFramework) False - 17.12.6 - \ No newline at end of file + diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs index 29ac848cd132..95a823165073 100644 --- a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs @@ -37,7 +37,7 @@ app.Run(); -internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +internal record WeatherForecast(DateOnly Date, int TemperatureC, string Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs index 4e0bf9d4cee6..7ef5c7a828ee 100644 --- a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs @@ -1,8 +1,13 @@ var builder = DistributedApplication.CreateBuilder(args); -var apiService = builder.AddProject("apiservice"); +var migration = builder.AddProject("migrationservice"); + +var apiService = builder + .AddProject("apiservice") + .WaitForCompletion(migration); builder.AddProject("webfrontend") + .WaitForCompletion(migration) .WithExternalHttpEndpoints() .WithReference(apiService) .WaitFor(apiService); diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj index 90483a02df60..3f394f6f83c0 100644 --- a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj @@ -9,9 +9,10 @@ - - - + + + + diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Program.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Program.cs new file mode 100644 index 000000000000..0abee487007d --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Program.cs @@ -0,0 +1,10 @@ +using MigrationService; +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Properties/launchSettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Properties/launchSettings.json new file mode 100644 index 000000000000..a312e8fb30bf --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "MigrationService": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/WatchAspire.MigrationService.csproj b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/WatchAspire.MigrationService.csproj new file mode 100644 index 000000000000..621a330ee1b4 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/WatchAspire.MigrationService.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + enable + enable + + + + + \ No newline at end of file diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Worker.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Worker.cs new file mode 100644 index 000000000000..4792261219db --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/Worker.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace MigrationService; + +public class Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : BackgroundService +{ + private static readonly ActivitySource s_activitySource = new("Migrations"); + + protected override Task ExecuteAsync(CancellationToken cancellationToken) + { + using var activity = s_activitySource.StartActivity( + "Migrating database", + ActivityKind.Client + ); + + logger.LogInformation("Migration complete"); + + hostApplicationLifetime.StopApplication(); + + return Task.CompletedTask; + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.Development.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.Development.json new file mode 100644 index 000000000000..cd7d0bc9100b --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.MigrationService/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.Wasm/App.razor b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.Wasm/App.razor index 13f3043f0c49..eba23da9b5ae 100644 --- a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.Wasm/App.razor +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.Wasm/App.razor @@ -1,4 +1,4 @@ - + diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.slnx b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.slnx new file mode 100644 index 000000000000..d9a238e555dc --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs index f1d2a3b77bbd..7171e0a19260 100644 --- a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs +++ b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs @@ -8,6 +8,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using System.Runtime.InteropServices; // diff --git a/test/TestAssets/TestProjects/WatchRazorWithDeps/RazorApp/Components/Pages/Home.razor b/test/TestAssets/TestProjects/WatchRazorWithDeps/RazorApp/Components/Pages/Home.razor index bb44fc2639e7..2daa1cadc252 100644 --- a/test/TestAssets/TestProjects/WatchRazorWithDeps/RazorApp/Components/Pages/Home.razor +++ b/test/TestAssets/TestProjects/WatchRazorWithDeps/RazorApp/Components/Pages/Home.razor @@ -5,4 +5,11 @@ -Welcome to your new app. \ No newline at end of file +Welcome to your new app. + +@code{ + class C + { + /* member placeholder */ + } +} \ No newline at end of file diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#EditorConfig-file#-n#item.verified/EditorConfig-file/.editorconfig b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#EditorConfig-file#-n#item.verified/EditorConfig-file/.editorconfig index 37f826f66406..fa2cc7622c20 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#EditorConfig-file#-n#item.verified/EditorConfig-file/.editorconfig +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#EditorConfig-file#-n#item.verified/EditorConfig-file/.editorconfig @@ -9,7 +9,7 @@ indent_style = space indent_size = 2 # Xml project files -[*.{csproj,fsproj,vbproj,proj}] +[*.{csproj,fsproj,vbproj,proj,slnx}] indent_size = 2 # Xml config files diff --git a/test/dotnet-watch-test-browser/Program.cs b/test/dotnet-watch-test-browser/Program.cs new file mode 100644 index 000000000000..78f96cb73ccb --- /dev/null +++ b/test/dotnet-watch-test-browser/Program.cs @@ -0,0 +1,146 @@ +using System; +using System.Buffers; +using System.Linq; +using System.Net.Http; +using System.Net.WebSockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +if (args is not [var urlArg]) +{ + Console.Error.WriteLine(); + return -1; +} + +Log($"Test browser opened at '{urlArg}'."); + +var url = new Uri(urlArg, UriKind.Absolute); + +var (webSocketUrls, publicKey) = await GetWebSocketUrlsAndPublicKey(url); + +var secret = RandomNumberGenerator.GetBytes(32); +var encryptedSecret = GetEncryptedSecret(publicKey, secret); + +using var webSocket = await OpenWebSocket(webSocketUrls, encryptedSecret); +var buffer = new byte[8 * 1024]; + +while (await TryReceiveMessageAsync(webSocket, message => Log($"Received: {Encoding.UTF8.GetString(message)}"))) +{ +} + +Log("WebSocket closed"); + +return 0; + +static async Task OpenWebSocket(string[] urls, string encryptedSecret) +{ + foreach (var url in urls) + { + try + { + var webSocket = new ClientWebSocket(); + webSocket.Options.AddSubProtocol(Uri.EscapeDataString(encryptedSecret)); + await webSocket.ConnectAsync(new Uri(url), CancellationToken.None); + return webSocket; + } + catch (Exception e) + { + Log($"Error connecting to '{url}': {e.Message}"); + } + } + + throw new InvalidOperationException("Unable to establish a connection."); +} + +static async ValueTask TryReceiveMessageAsync(WebSocket socket, Action> receiver) +{ + var writer = new ArrayBufferWriter(initialCapacity: 1024); + + while (true) + { + ValueWebSocketReceiveResult result; + var data = writer.GetMemory(); + try + { + result = await socket.ReceiveAsync(data, CancellationToken.None); + } + catch (Exception e) when (e is not OperationCanceledException) + { + Log($"Failed to receive response: {e.Message}"); + return false; + } + + if (result.MessageType == WebSocketMessageType.Close) + { + return false; + } + + writer.Advance(result.Count); + if (result.EndOfMessage) + { + break; + } + } + + receiver(writer.WrittenSpan); + return true; +} + +static async Task<(string[] url, string key)> GetWebSocketUrlsAndPublicKey(Uri baseUrl) +{ + var refreshScriptUrl = new Uri(baseUrl, "/_framework/aspnetcore-browser-refresh.js"); + + Log($"Fetching: {refreshScriptUrl}"); + + using var httpClient = new HttpClient(); + var content = await httpClient.GetStringAsync(refreshScriptUrl); + + Log($"Request for '{refreshScriptUrl}' succeeded"); + var webSocketUrl = GetWebSocketUrls(content); + var key = GetSharedSecretKey(content); + + Log($"WebSocket urls are '{string.Join(',', webSocketUrl)}'."); + Log($"Key is '{key}'."); + + return (webSocketUrl, key); +} + +static string[] GetWebSocketUrls(string refreshScript) +{ + var pattern = "const webSocketUrls = '([^']+)'"; + + var match = Regex.Match(refreshScript, pattern); + if (!match.Success) + { + throw new InvalidOperationException($"Can't find web socket URL pattern in the script: {pattern}{Environment.NewLine}{refreshScript}"); + } + + return match.Groups[1].Value.Split(","); +} + +static string GetSharedSecretKey(string refreshScript) +{ + var pattern = @"const sharedSecret = await getSecret\('([^']+)'\)"; + + var match = Regex.Match(refreshScript, pattern); + if (!match.Success) + { + throw new InvalidOperationException($"Can't find web socket shared secret pattern in the script: {pattern}{Environment.NewLine}{refreshScript}"); + } + + return match.Groups[1].Value; +} + +// Equivalent to getSecret function in WebSocketScriptInjection.js: +static string GetEncryptedSecret(string key, byte[] secret) +{ + using var rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(key), out _); + return Convert.ToBase64String(rsa.Encrypt(secret, RSAEncryptionPadding.OaepSHA256)); +} + +static void Log(string message) + => Console.WriteLine($"🧪 {message}"); diff --git a/test/dotnet-watch-test-browser/dotnet-watch-test-browser.csproj b/test/dotnet-watch-test-browser/dotnet-watch-test-browser.csproj new file mode 100644 index 000000000000..408cb9159c99 --- /dev/null +++ b/test/dotnet-watch-test-browser/dotnet-watch-test-browser.csproj @@ -0,0 +1,14 @@ + + + Exe + $(ToolsetTargetFramework) + MicrosoftAspNetCore + Microsoft.DotNet.Watch.UnitTests + + + + + + + + diff --git a/test/dotnet-watch.Tests/Browser/BrowserLaunchTests.cs b/test/dotnet-watch.Tests/Browser/BrowserLaunchTests.cs deleted file mode 100644 index f5fbf358d819..000000000000 --- a/test/dotnet-watch.Tests/Browser/BrowserLaunchTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.DotNet.Watch.UnitTests -{ - public class BrowserLaunchTests : DotNetWatchTestBase - { - private const string AppName = "WatchBrowserLaunchApp"; - - public BrowserLaunchTests(ITestOutputHelper logger) - : base(logger) - { - } - - [Fact] - public async Task LaunchesBrowserOnStart() - { - var testAsset = TestAssets.CopyTestAsset(AppName) - .WithSource(); - - App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); - - // check that all app output is printed out: - await App.WaitForOutputLineContaining("Content root path:"); - - Assert.Contains(App.Process.Output, line => line.Contains("Application started. Press Ctrl+C to shut down.")); - Assert.Contains(App.Process.Output, line => line.Contains("Hosting environment: Development")); - - // Verify we launched the browser. - Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001")); - } - - [Fact] - public async Task UsesBrowserSpecifiedInEnvironment() - { - var testAsset = TestAssets.CopyTestAsset(AppName) - .WithSource(); - - App.EnvironmentVariables.Add("DOTNET_WATCH_BROWSER_PATH", "mycustombrowser.bat"); - - App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToUseBrowserRefresh); - await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToLaunchBrowser); - - // Verify we launched the browser. - await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: mycustombrowser.bat https://localhost:5001"); - } - } -} diff --git a/test/dotnet-watch.Tests/Browser/BrowserRefreshServerTests.cs b/test/dotnet-watch.Tests/Browser/BrowserRefreshServerTests.cs new file mode 100644 index 000000000000..236c99ec077d --- /dev/null +++ b/test/dotnet-watch.Tests/Browser/BrowserRefreshServerTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.HotReload; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class BrowserRefreshServerTests +{ + class TestListener : IDisposable + { + public void Dispose() + { + } + } + + [Theory] + [CombinatorialData] + public async Task ConfigureLaunchEnvironmentAsync(LogLevel logLevel, bool enableHotReload) + { + var middlewarePath = Path.GetTempPath(); + var middlewareFileName = Path.GetFileNameWithoutExtension(middlewarePath); + + var server = new TestBrowserRefreshServer(middlewarePath) + { + CreateAndStartHostImpl = () => new WebServerHost(new TestListener(), ["http://test.endpoint"], virtualDirectory: "/test/virt/dir") + }; + + ((TestLogger)server.Logger).IsEnabledImpl = level => level == logLevel; + + await server.StartAsync(CancellationToken.None); + + var envBuilder = new Dictionary(); + server.ConfigureLaunchEnvironment(envBuilder, enableHotReload); + + Assert.True(envBuilder.Remove("ASPNETCORE_AUTO_RELOAD_WS_KEY")); + + var expected = new List() + { + "ASPNETCORE_AUTO_RELOAD_VDIR=/test/virt/dir", + "ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT=http://test.endpoint", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=" + middlewareFileName, + "DOTNET_STARTUP_HOOKS=" + middlewarePath, + }; + + if (enableHotReload) + { + expected.Add("DOTNET_MODIFIABLE_ASSEMBLIES=debug"); + } + + if (logLevel == LogLevel.Trace) + { + expected.Add("Logging__LogLevel__Microsoft.AspNetCore.Watch=Debug"); + } + + AssertEx.SequenceEqual(expected.Order(), envBuilder.OrderBy(e => e.Key).Select(e => $"{e.Key}={e.Value}")); + } +} diff --git a/test/dotnet-watch.Tests/Browser/BrowserTests.cs b/test/dotnet-watch.Tests/Browser/BrowserTests.cs new file mode 100644 index 000000000000..8fa4c00de4ef --- /dev/null +++ b/test/dotnet-watch.Tests/Browser/BrowserTests.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class BrowserTests(ITestOutputHelper logger) : DotNetWatchTestBase(logger) +{ + [Fact] + public async Task LaunchesBrowserOnStart() + { + var testAsset = TestAssets.CopyTestAsset("WatchBrowserLaunchApp") + .WithSource(); + + App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); + + // check that all app output is printed out: + await App.WaitForOutputLineContaining("Content root path:"); + + Assert.Contains(App.Process.Output, line => line.Contains("Application started. Press Ctrl+C to shut down.")); + Assert.Contains(App.Process.Output, line => line.Contains("Hosting environment: Development")); + + // Verify we launched the browser. + App.AssertOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage("https://localhost:5001", "")); + } + + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 + public async Task BrowserDiagnostics() + { + var testAsset = TestAssets.CopyTestAsset("WatchRazorWithDeps") + .WithSource(); + + App.UseTestBrowser(); + + var url = $"http://localhost:{TestOptions.GetTestPort()}"; + var tfm = ToolsetInfo.CurrentTargetFramework; + + App.Start(testAsset, ["--urls", url], relativeProjectDirectory: "RazorApp", testFlags: TestFlags.ReadKeyFromStdin); + + await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + + // Verify the browser has been launched. + await App.WaitUntilOutputContains($"🧪 Test browser opened at '{url}'."); + + // Verify the browser connected to the refresh server. + await App.WaitUntilOutputContains(MessageDescriptor.ConnectedToRefreshServer, "Browser #1"); + + App.Process.ClearOutput(); + + var homePagePath = Path.Combine(testAsset.Path, "RazorApp", "Components", "Pages", "Home.razor"); + + // rude edit: + UpdateSourceFile(homePagePath, src => src.Replace("/* member placeholder */", """ + public virtual int F() => 1; + """)); + + var errorMessage = $"{homePagePath}(13,9): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."; + var jsonErrorMessage = JsonSerializer.Serialize(errorMessage); + + await App.WaitForOutputLineContaining(errorMessage); + + await App.WaitForOutputLineContaining("Do you want to restart your app?"); + + await App.WaitUntilOutputContains($$""" + 🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":[{{jsonErrorMessage}}]} + """); + + // auto restart next time: + App.SendKey('a'); + + // browser page is reloaded when the app restarts: + await App.WaitForOutputLineContaining(MessageDescriptor.ReloadingBrowser, $"RazorApp ({tfm})"); + + // browser page was reloaded after the app restarted: + await App.WaitUntilOutputContains(""" + 🧪 Received: Reload + """); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + + // another rude edit: + UpdateSourceFile(homePagePath, src => src.Replace("public virtual int F() => 1;", "/* member placeholder */")); + + errorMessage = $"{homePagePath}(11,5): error ENC0033: Deleting method 'F()' requires restarting the application."; + await App.WaitForOutputLineContaining("[auto-restart] " + errorMessage); + + await App.WaitUntilOutputContains($$""" + 🧪 Received: {"type":"HotReloadDiagnosticsv1","diagnostics":["Restarting application to apply changes ..."]} + """); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + + // browser page was reloaded after the app restarted: + await App.WaitUntilOutputContains(""" + 🧪 Received: Reload + """); + + // valid edit: + UpdateSourceFile(homePagePath, src => src.Replace("/* member placeholder */", "public int F() => 1;")); + + await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); + + await App.WaitUntilOutputContains($$""" + 🧪 Received: {"type":"AspNetCoreHotReloadApplied"} + """); + } +} diff --git a/test/dotnet-watch.Tests/Build/EvaluationTests.cs b/test/dotnet-watch.Tests/Build/EvaluationTests.cs index 5a6786875334..4c1bcbdbb30a 100644 --- a/test/dotnet-watch.Tests/Build/EvaluationTests.cs +++ b/test/dotnet-watch.Tests/Build/EvaluationTests.cs @@ -236,8 +236,13 @@ public async Task ProjectReferences_OneLevel() var project1 = new TestProject("Project1") { + IsExe = true, TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462", ReferencedProjects = { project2 }, + SourceFiles = + { + { "Project1.cs", s_emptyProgram }, + }, }; var testAsset = _testAssets.CreateTestProject(project1); @@ -271,8 +276,13 @@ public async Task TransitiveProjectReferences_TwoLevels() var project1 = new TestProject("Project1") { + IsExe = true, TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462", ReferencedProjects = { project2 }, + SourceFiles = + { + { "Project1.cs", s_emptyProgram }, + }, }; var testAsset = _testAssets.CreateTestProject(project1); @@ -305,8 +315,13 @@ public async Task SingleTargetRoot_MultiTargetedDependency(bool specifyTargetFra var project1 = new TestProject("Project1") { + IsExe = true, TargetFrameworks = ToolsetInfo.CurrentTargetFramework, ReferencedProjects = { project2 }, + SourceFiles = + { + { "Project1.cs", s_emptyProgram }, + }, }; var testAsset = _testAssets.CreateTestProject(project1, identifier: specifyTargetFramework.ToString()); @@ -479,8 +494,13 @@ public async Task MsbuildOutput() var project1 = new TestProject("Project1") { + IsExe = true, TargetFrameworks = "net462", ReferencedProjects = { project2 }, + SourceFiles = + { + { "Program.cs", s_emptyProgram }, + }, }; var testAsset = _testAssets.CreateTestProject(project1); @@ -495,9 +515,9 @@ public async Task MsbuildOutput() Assert.Null(result); // note: msbuild prints errors to stdout, we match the pattern and report as error: - AssertEx.Equal( + Assert.Contains( $"[Error] {project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)", - _logger.GetAndClearMessages().Single(m => m.Contains("error NU1201"))); + _logger.GetAndClearMessages()); } private readonly struct ExpectedFile(string path, string? staticAssetUrl = null, bool targetsOnly = false, bool graphOnly = false) diff --git a/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs b/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs index 24ee2076885b..95bbd1d79657 100644 --- a/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs +++ b/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs @@ -92,22 +92,15 @@ public async Task RunsWithIterationEnvVariable() App.Start(testAsset, []); - await App.AssertStarted(); + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); - var source = Path.Combine(testAsset.Path, "Program.cs"); - var contents = File.ReadAllText(source); - const string messagePrefix = "DOTNET_WATCH_ITERATION = "; + App.AssertOutputContains("DOTNET_WATCH_ITERATION = 1"); + App.Process.ClearOutput(); - var value = await App.AssertOutputLineStartsWith(messagePrefix); - Assert.Equal(1, int.Parse(value, CultureInfo.InvariantCulture)); + UpdateSourceFile(Path.Combine(testAsset.Path, "Program.cs")); await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); - - UpdateSourceFile(source); - await App.AssertStarted(); - - value = await App.AssertOutputLineStartsWith(messagePrefix); - Assert.Equal(2, int.Parse(value, CultureInfo.InvariantCulture)); + App.AssertOutputContains("DOTNET_WATCH_ITERATION = 2"); } [Fact] diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index a7a1bcda1923..501ce980b20f 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -429,7 +429,7 @@ public async Task AutoRestartOnRudeEdit(bool nonInteractive) await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); App.Process.ClearOutput(); @@ -440,6 +440,64 @@ public async Task AutoRestartOnRudeEdit(bool nonInteractive) await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); } + [Theory] + [CombinatorialData] + public async Task AutoRestartOnRuntimeRudeEdit(bool nonInteractive) + { + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp") + .WithSource(); + + var tfm = ToolsetInfo.CurrentTargetFramework; + var programPath = Path.Combine(testAsset.Path, "Program.cs"); + + // Changes the type of lambda without updating top-level code. + // The loop will end up calling the old version of the lambda resulting in runtime rude edit. + + File.WriteAllText(programPath, """ + using System; + using System.Threading; + + var d = C.F(); + + while (true) + { + Thread.Sleep(250); + d(1); + } + + class C + { + public static Action F() + { + return a => + { + Console.WriteLine(a.GetType()); + }; + } + } + """); + + App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains("System.Int32"); + App.Process.ClearOutput(); + + UpdateSourceFile(programPath, src => src.Replace("Action", "Action")); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains("System.Byte"); + + App.AssertOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] HotReloadException handler installed."); + App.AssertOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Runtime rude edit detected:"); + + App.AssertOutputContains($"dotnet watch ⚠ [WatchHotReloadApp ({tfm})] " + + "Attempted to invoke a deleted lambda or local function implementation. " + + "This can happen when lambda or local function is deleted while the application is running."); + + App.AssertOutputContains(MessageDescriptor.RestartingApplication, $"WatchHotReloadApp ({tfm})"); + } + [Fact] public async Task AutoRestartOnRudeEditAfterRestartPrompt() { @@ -459,7 +517,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() await App.AssertOutputLineStartsWith(" ❔ Do you want to restart your app? Yes (y) / No (n) / Always (a) / Never (v)", failure: _ => false); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"❌ {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + App.AssertOutputContains($"❌ {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); App.Process.ClearOutput(); App.SendKey('a'); @@ -476,7 +534,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,1): error ENC0033: Deleting method 'F()' requires restarting the application."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,1): error ENC0033: Deleting method 'F()' requires restarting the application."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); } @@ -514,7 +572,7 @@ public async Task AutoRestartOnNoEffectEdit(bool nonInteractive) await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(16,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(17,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); App.AssertOutputContains(""); @@ -721,6 +779,8 @@ class AppUpdateHandler [PlatformSpecificFact(TestPlatforms.Windows)] public async Task GracefulTermination_Windows() { + var tfm = ToolsetInfo.CurrentTargetFramework; + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp") .WithSource(); @@ -739,7 +799,7 @@ public async Task GracefulTermination_Windows() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - await App.WaitUntilOutputContains(new Regex(@"dotnet watch 🕵️ \[.*\] Windows Ctrl\+C handling enabled.")); + await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Windows Ctrl+C handling enabled."); await App.WaitUntilOutputContains("Started"); @@ -749,7 +809,38 @@ public async Task GracefulTermination_Windows() await App.WaitUntilOutputContains("exited with exit code 0."); } - [PlatformSpecificTheory(TestPlatforms.Windows, Skip = "https://github.com/dotnet/sdk/issues/49928")] // https://github.com/dotnet/sdk/issues/49307 + [PlatformSpecificFact(TestPlatforms.AnyUnix)] + public async Task GracefulTermination_Unix() + { + var tfm = ToolsetInfo.CurrentTargetFramework; + + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp") + .WithSource(); + + var programPath = Path.Combine(testAsset.Path, "Program.cs"); + + UpdateSourceFile(programPath, src => src.Replace("// ", """ + using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => + { + Console.WriteLine("SIGTERM detected! Performing cleanup..."); + }); + """)); + + App.Start(testAsset, [], testFlags: TestFlags.ReadKeyFromStdin); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + + await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Posix signal handlers registered."); + + await App.WaitUntilOutputContains("Started"); + + App.SendControlC(); + + await App.WaitForOutputLineContaining("SIGTERM detected! Performing cleanup..."); + await App.WaitUntilOutputContains("exited with exit code 0."); + } + + [PlatformSpecificTheory(TestPlatforms.Windows, Skip = "https://github.com/dotnet/sdk/issues/49928")] // https://github.com/dotnet/aspnetcore/issues/63759 [CombinatorialData] public async Task BlazorWasm(bool projectSpecifiesCapabilities) { @@ -777,7 +868,7 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); // Browser is launched based on blazor-devserver output "Now listening on: ...". - await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}"); + await App.WaitUntilOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); // Middleware should have been loaded to blazor-devserver before the browser is launched: App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorWasmHotReloadMiddleware[0]"); @@ -809,7 +900,7 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) } } - [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task BlazorWasm_MSBuildWarning() { var testAsset = TestAssets @@ -831,7 +922,7 @@ public async Task BlazorWasm_MSBuildWarning() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); } - [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task BlazorWasm_Restart() { var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasm") @@ -847,14 +938,14 @@ public async Task BlazorWasm_Restart() App.AssertOutputContains(MessageDescriptor.PressCtrlRToRestart); // Browser is launched based on blazor-devserver output "Now listening on: ...". - await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}"); + await App.WaitUntilOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); App.SendControlR(); await App.WaitUntilOutputContains(MessageDescriptor.ReloadingBrowser); } - [PlatformSpecificFact(TestPlatforms.Windows, Skip = "https://github.com/dotnet/sdk/issues/49928")] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows, Skip = "https://github.com/dotnet/sdk/issues/49928")] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task BlazorWasmHosted() { var testAsset = TestAssets.CopyTestAsset("WatchBlazorWasmHosted") @@ -878,7 +969,7 @@ public async Task BlazorWasmHosted() App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Capabilities: 'Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva'"); } - [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task Razor_Component_ScopedCssAndStaticAssets() { var testAsset = TestAssets.CopyTestAsset("WatchRazorWithDeps") @@ -891,7 +982,7 @@ public async Task Razor_Component_ScopedCssAndStaticAssets() App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); - App.AssertOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}"); + App.AssertOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); App.Process.ClearOutput(); var scopedCssPath = Path.Combine(testAsset.Path, "RazorClassLibrary", "Components", "Example.razor.css"); @@ -1114,7 +1205,7 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath await App.AssertOutputLineStartsWith("> NewSubdir", failure: _ => false); } - [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task Aspire_BuildError_ManualRestart() { var tfm = ToolsetInfo.CurrentTargetFramework; @@ -1135,8 +1226,13 @@ public async Task Aspire_BuildError_ManualRestart() // check that Aspire server output is logged via dotnet-watch reporter: await App.WaitUntilOutputContains("dotnet watch ⭐ Now listening on:"); - // wait until after DCP session started: - await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #1"); + // wait until after all DCP sessions have started: + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); + App.AssertOutputContains("dotnet watch ⭐ Session started: #1"); + App.AssertOutputContains("dotnet watch ⭐ Session started: #2"); + + // MigrationService terminated: + App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); // working directory of the service should be it's project directory: await App.WaitUntilOutputContains($"ApiService working directory: '{Path.GetDirectoryName(serviceProjectPath)}'"); @@ -1176,9 +1272,7 @@ public async Task Aspire_BuildError_ManualRestart() App.AssertOutputContains("Application is shutting down..."); - // We don't have means to gracefully terminate process on Windows, see https://github.com/dotnet/runtime/issues/109432 App.AssertOutputContains($"[WatchAspire.ApiService ({tfm})] Exited"); - App.AssertOutputContains(new Regex(@"dotnet watch ⌚ \[WatchAspire.ApiService \(net.*\)\] Process id [0-9]+ ran for [0-9]+ms and exited with exit code 0")); App.AssertOutputContains(MessageDescriptor.Building.GetMessage(serviceProjectPath)); App.AssertOutputContains("error CS0246: The type or namespace name 'WeatherForecast' could not be found"); @@ -1189,11 +1283,12 @@ public async Task Aspire_BuildError_ManualRestart() serviceSourcePath, serviceSource.Replace("WeatherForecast", "WeatherForecast2")); - await App.WaitForOutputLineContaining(MessageDescriptor.Capabilities, $"WatchAspire.ApiService ({tfm})"); + await App.WaitForOutputLineContaining(MessageDescriptor.ProjectsRestarted.GetMessage(1)); App.AssertOutputContains(MessageDescriptor.BuildSucceeded.GetMessage(serviceProjectPath)); App.AssertOutputContains(MessageDescriptor.ProjectsRebuilt); App.AssertOutputContains($"dotnet watch ⭐ Starting project: {serviceProjectPath}"); + App.Process.ClearOutput(); App.SendControlC(); @@ -1201,16 +1296,17 @@ public async Task Aspire_BuildError_ManualRestart() await App.WaitUntilOutputContains($"[WatchAspire.ApiService ({tfm})] Exited"); await App.WaitUntilOutputContains($"[WatchAspire.AppHost ({tfm})] Exited"); - await App.WaitUntilOutputContains(new Regex(@"dotnet watch ⌚ \[WatchAspire.ApiService \(net.*\)\] Process id [0-9]+ ran for [0-9]+ms and exited with exit code 0")); - await App.WaitUntilOutputContains(new Regex(@"dotnet watch ⌚ \[WatchAspire.AppHost \(net.*\)\] Process id [0-9]+ ran for [0-9]+ms and exited with exit code 0")); await App.WaitUntilOutputContains("dotnet watch ⭐ Waiting for server to shutdown ..."); App.AssertOutputContains("dotnet watch ⭐ Stop session #1"); - App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); + App.AssertOutputContains("dotnet watch ⭐ Stop session #2"); + App.AssertOutputContains("dotnet watch ⭐ Stop session #3"); + App.AssertOutputContains("dotnet watch ⭐ [#2] Sending 'sessionTerminated'"); + App.AssertOutputContains("dotnet watch ⭐ [#3] Sending 'sessionTerminated'"); } - [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307") + [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 public async Task Aspire_NoEffect_AutoRestart() { var tfm = ToolsetInfo.CurrentTargetFramework; @@ -1226,18 +1322,32 @@ public async Task Aspire_NoEffect_AutoRestart() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); // wait until after DCP sessions have been started for all projects: - await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #2"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); + + // other services are waiting for completion of MigrationService: + App.AssertOutputContains("dotnet watch ⭐ Session started: #1"); + App.AssertOutputContains(MessageDescriptor.Exited, $"WatchAspire.MigrationService ({tfm})"); + App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); + + // migration service output should not be printed to dotnet-watch output, it hsould be sent via DCP as a notification: + App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'serviceLogs': log_message=' Migration complete', is_std_err=False"); + App.AssertOutputDoesNotContain(new Regex("^ +Migration complete")); + App.Process.ClearOutput(); // no-effect edit: UpdateSourceFile(webSourcePath, src => src.Replace("/* top-level placeholder */", "builder.Services.AddRazorComponents();")); await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); - await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #2"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); App.AssertOutputContains(MessageDescriptor.ProjectsRestarted.GetMessage(1)); App.AssertOutputDoesNotContain("⚠"); + // The process exited and should not participate in Hot Reload: + App.AssertOutputDoesNotContain($"[WatchAspire.MigrationService ({tfm})]"); + App.AssertOutputDoesNotContain("dotnet watch ⭐ [#1]"); + App.Process.ClearOutput(); // lambda body edit: @@ -1245,9 +1355,13 @@ public async Task Aspire_NoEffect_AutoRestart() await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); App.AssertOutputContains($"dotnet watch 🕵️ [WatchAspire.Web ({tfm})] Updates applied."); - App.AssertOutputDoesNotContain("Projects rebuilt"); - App.AssertOutputDoesNotContain("Projects restarted"); + App.AssertOutputDoesNotContain(MessageDescriptor.ProjectsRebuilt); + App.AssertOutputDoesNotContain(MessageDescriptor.ProjectsRestarted); App.AssertOutputDoesNotContain("⚠"); + + // The process exited and should not participate in Hot Reload: + App.AssertOutputDoesNotContain($"[WatchAspire.MigrationService ({tfm})]"); + App.AssertOutputDoesNotContain("dotnet watch ⭐ [#1]"); } } } diff --git a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs index bedd9d2a7c5c..9ac5030c4eba 100644 --- a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs +++ b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs @@ -25,7 +25,7 @@ public async Task ReferenceOutputAssembly_False() var loggerFactory = new LoggerFactory(reporter); var logger = loggerFactory.CreateLogger("Test"); var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(options.ProjectPath, globalOptions: [], logger, projectGraphRequired: false, CancellationToken.None); - var handler = new CompilationHandler(loggerFactory, logger, processRunner); + var handler = new CompilationHandler(logger, processRunner); await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None); diff --git a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs index 76993fbe54e3..ff2c8491194f 100644 --- a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs +++ b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs @@ -78,6 +78,7 @@ private static async Task Launch(string projectPath, TestRuntime projectOptions, new CancellationTokenSource(), onOutput: null, + onExit: null, restartOperation: startOp!, cancellationToken); @@ -525,7 +526,7 @@ public async Task RudeEditInProjectWithoutRunningProcess() // Terminate the process: Log($"Terminating process {runningProject.ProjectNode.GetDisplayName()} ..."); - await w.Service.ProjectLauncher.TerminateProcessAsync(runningProject, CancellationToken.None); + await runningProject.TerminateAsync(); // rude edit in A (changing assembly level attribute): UpdateSourceFile(serviceSourceA2, """ diff --git a/test/dotnet-watch.Tests/TestUtilities/AssertEx.cs b/test/dotnet-watch.Tests/TestUtilities/AssertEx.cs index 6f2b2fc7e30a..c80299e56b73 100644 --- a/test/dotnet-watch.Tests/TestUtilities/AssertEx.cs +++ b/test/dotnet-watch.Tests/TestUtilities/AssertEx.cs @@ -117,11 +117,11 @@ public static void Equal(T expected, T actual, IEqualityComparer? comparer if (expected == null) { - Fail("expected was null, but actual wasn't" + Environment.NewLine + message); + Fail("pattern was null, but actual wasn't" + Environment.NewLine + message); } else if (actual == null) { - Fail("actual was null, but expected wasn't" + Environment.NewLine + message); + Fail("actual was null, but pattern wasn't" + Environment.NewLine + message); } else if (!(comparer ?? AssertEqualityComparer.Instance).Equals(expected, actual)) { @@ -235,14 +235,24 @@ public static void EqualFileList(IEnumerable expectedFiles, IEnumerable< } public static void ContainsSubstring(string expected, IEnumerable items) + => AssertSubstringPresence(expected, items, expectedPresent: true); + + public static void DoesNotContainSubstring(string expected, IEnumerable items) + => AssertSubstringPresence(expected, items, expectedPresent: false); + + private static void AssertSubstringPresence(string expected, IEnumerable items, bool expectedPresent) { - if (items.Any(item => item.Contains(expected))) + if (items.Any(item => item.Contains(expected)) == expectedPresent) { return; } var message = new StringBuilder(); - message.AppendLine($"Expected output not found:"); + + message.AppendLine(expectedPresent + ? "Expected text not found in the output:" + : "Text not expected to be found in the output:"); + message.AppendLine(expected); message.AppendLine(); message.AppendLine("Actual output:"); @@ -256,15 +266,25 @@ public static void ContainsSubstring(string expected, IEnumerable items) } public static void ContainsPattern(Regex expected, IEnumerable items) + => AssertPatternPresence(expected, items, expectedPresent: true); + + public static void DoesNotContainPattern(Regex pattern, IEnumerable items) + => AssertPatternPresence(pattern, items, expectedPresent: false); + + private static void AssertPatternPresence(Regex pattern, IEnumerable items, bool expectedPresent) { - if (items.Any(item => expected.IsMatch(item))) + if (items.Any(item => pattern.IsMatch(item)) == expectedPresent) { return; } var message = new StringBuilder(); - message.AppendLine($"Expected pattern not found in the output:"); - message.AppendLine(expected.ToString()); + + message.AppendLine(expectedPresent + ? "Expected pattern found in the output:" + : "Pattern not expected to be found in the output:"); + + message.AppendLine(pattern.ToString()); message.AppendLine(); message.AppendLine("Actual output:"); diff --git a/test/dotnet-watch.Tests/TestUtilities/TestBrowserRefreshServer.cs b/test/dotnet-watch.Tests/TestUtilities/TestBrowserRefreshServer.cs new file mode 100644 index 000000000000..06acc9719b39 --- /dev/null +++ b/test/dotnet-watch.Tests/TestUtilities/TestBrowserRefreshServer.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.HotReload; + +namespace Microsoft.DotNet.Watch.UnitTests; + +internal class TestBrowserRefreshServer(string middlewareAssemblyPath) + : AbstractBrowserRefreshServer(middlewareAssemblyPath, new TestLogger(), new TestLoggerFactory()) +{ + public Func? CreateAndStartHostImpl; + + protected override ValueTask CreateAndStartHostAsync(CancellationToken cancellationToken) + => ValueTask.FromResult((CreateAndStartHostImpl ?? throw new NotImplementedException())()); + + protected override bool SuppressTimeouts => true; +} diff --git a/test/dotnet-watch.Tests/TestUtilities/TestLogger.cs b/test/dotnet-watch.Tests/TestUtilities/TestLogger.cs index 3a9add553aa0..5ae8e7dcd811 100644 --- a/test/dotnet-watch.Tests/TestUtilities/TestLogger.cs +++ b/test/dotnet-watch.Tests/TestUtilities/TestLogger.cs @@ -11,6 +11,8 @@ internal class TestLogger(ITestOutputHelper? output = null) : ILogger public readonly Lock Guard = new(); private readonly List _messages = []; + public Func? IsEnabledImpl; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { var message = $"[{logLevel}] {formatter(state, exception)}"; @@ -36,5 +38,5 @@ public ImmutableArray GetAndClearMessages() where TState : notnull => throw new NotImplementedException(); public bool IsEnabled(LogLevel logLevel) - => true; + => IsEnabledImpl?.Invoke(logLevel) ?? true; } diff --git a/test/dotnet-watch.Tests/TestUtilities/TestLoggerFactory.cs b/test/dotnet-watch.Tests/TestUtilities/TestLoggerFactory.cs new file mode 100644 index 000000000000..be1e69682952 --- /dev/null +++ b/test/dotnet-watch.Tests/TestUtilities/TestLoggerFactory.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Watch.UnitTests; + +internal class TestLoggerFactory(ITestOutputHelper? output = null) : ILoggerFactory +{ + public Func? CreateLoggerImpl; + + public ILogger CreateLogger(string categoryName) + => CreateLoggerImpl?.Invoke(categoryName) ?? new TestLogger(output); + + public void AddProvider(ILoggerProvider provider) {} + public void Dispose() { } +} diff --git a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs index 8888b5634398..81bfd17aae20 100644 --- a/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs +++ b/test/dotnet-watch.Tests/TestUtilities/WatchableApp.cs @@ -32,15 +32,21 @@ internal sealed class WatchableApp(DebugTestOutputLogger logger) : IDisposable public void AssertOutputContains(string message) => AssertEx.ContainsSubstring(message, Process.Output); - public void AssertOutputDoesNotContain(string message) - => Assert.DoesNotContain(Process.Output, line => line.Contains(message)); - public void AssertOutputContains(Regex pattern) => AssertEx.ContainsPattern(pattern, Process.Output); public void AssertOutputContains(MessageDescriptor descriptor, string projectDisplay = null) => AssertOutputContains(GetPattern(descriptor, projectDisplay)); + public void AssertOutputDoesNotContain(string message) + => AssertEx.DoesNotContainSubstring(message, Process.Output); + + public void AssertOutputDoesNotContain(Regex pattern) + => AssertEx.DoesNotContainPattern(pattern, Process.Output); + + public void AssertOutputDoesNotContain(MessageDescriptor descriptor, string projectDisplay = null) + => AssertOutputDoesNotContain(GetPattern(descriptor, projectDisplay)); + private static Regex GetPattern(MessageDescriptor descriptor, string projectDisplay = null) => new Regex(Regex.Replace(Regex.Escape((projectDisplay != null ? $"[{projectDisplay}] " : "") + descriptor.Format), @"\\\{[0-9]+\}", ".*")); @@ -203,5 +209,22 @@ public void SendKey(char c) Process.Process.StandardInput.Write(c); Process.Process.StandardInput.Flush(); } + + public void UseTestBrowser() + { + var path = GetTestBrowserPath(); + EnvironmentVariables.Add("DOTNET_WATCH_BROWSER_PATH", path); + + if (!OperatingSystem.IsWindows()) + { + File.SetUnixFileMode(path, UnixFileMode.UserExecute); + } + } + + public static string GetTestBrowserPath() + { + var exeExtension = OperatingSystem.IsWindows() ? ".exe" : string.Empty; + return Path.Combine(Path.GetDirectoryName(typeof(WatchableApp).Assembly.Location!)!, "test-browser", "dotnet-watch-test-browser" + exeExtension); + } } } diff --git a/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs b/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs index 1efccba2eb22..d673b618826c 100644 --- a/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs +++ b/test/dotnet-watch.Tests/Watch/BuildEvaluatorTests.cs @@ -16,9 +16,11 @@ private static DotNetWatchContext CreateContext(bool suppressMSBuildIncrementali SuppressMSBuildIncrementalism = suppressMSBuildIncrementalism }; + var processOutputReporter = new TestProcessOutputReporter(); + return new DotNetWatchContext() { - ProcessOutputReporter = new TestProcessOutputReporter(), + ProcessOutputReporter = processOutputReporter, Logger = NullLogger.Instance, BuildLogger = NullLogger.Instance, LoggerFactory = NullLoggerFactory.Instance, @@ -26,7 +28,7 @@ private static DotNetWatchContext CreateContext(bool suppressMSBuildIncrementali Options = new(), RootProjectOptions = TestOptions.ProjectOptions, EnvironmentOptions = environmentOptions, - BrowserLauncher = new BrowserLauncher(NullLogger.Instance, environmentOptions), + BrowserLauncher = new BrowserLauncher(NullLogger.Instance, processOutputReporter, environmentOptions), BrowserRefreshServerFactory = new BrowserRefreshServerFactory() }; } diff --git a/test/dotnet-watch.Tests/Watch/NoRestoreTests.cs b/test/dotnet-watch.Tests/Watch/NoRestoreTests.cs index a28d91c3423e..895a9d6b779e 100644 --- a/test/dotnet-watch.Tests/Watch/NoRestoreTests.cs +++ b/test/dotnet-watch.Tests/Watch/NoRestoreTests.cs @@ -13,9 +13,11 @@ private static DotNetWatchContext CreateContext(string[] args = null, Environmen { environmentOptions ??= TestOptions.GetEnvironmentOptions(); + var processOutputReporter = new TestProcessOutputReporter(); + return new() { - ProcessOutputReporter = new TestProcessOutputReporter(), + ProcessOutputReporter = processOutputReporter, LoggerFactory = NullLoggerFactory.Instance, Logger = NullLogger.Instance, BuildLogger = NullLogger.Instance, @@ -23,7 +25,7 @@ private static DotNetWatchContext CreateContext(string[] args = null, Environmen Options = new(), RootProjectOptions = TestOptions.GetProjectOptions(args), EnvironmentOptions = environmentOptions, - BrowserLauncher = new BrowserLauncher(NullLogger.Instance, environmentOptions), + BrowserLauncher = new BrowserLauncher(NullLogger.Instance, processOutputReporter, environmentOptions), BrowserRefreshServerFactory = new BrowserRefreshServerFactory() }; } diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 6a81d97e1ed3..772d35b80517 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -16,10 +16,23 @@ --> + + + + + + <_Files>@(TestBrowserOutput->'%(RootDir)%(Directory)*.*') + + + <_FileItem Include="$(_Files)" /> + + + + diff --git a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs index 2f131031b24e..bcb6a6dbfde1 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs @@ -11,10 +11,12 @@ public class FakeTelemetry : ITelemetry { public bool Enabled { get; set; } = true; + private readonly List _logEntries = new List(); + public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements) { - LogEntry = new LogEntry { EventName = eventName, Properties = properties, Measurement = measurements }; - + var entry = new LogEntry { EventName = eventName, Properties = properties, Measurement = measurements }; + _logEntries.Add(entry); } public void Flush() @@ -25,8 +27,8 @@ public void Dispose() { } - public LogEntry LogEntry { get; private set; } + public LogEntry LogEntry => _logEntries.Count > 0 ? _logEntries[_logEntries.Count - 1] : null; + public IReadOnlyList LogEntries => _logEntries.AsReadOnly(); } - } diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs index 08c5ee7071cd..8b6c7de5b074 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tests; using BuildCommand = Microsoft.DotNet.Cli.Commands.Build.BuildCommand; namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -10,8 +12,8 @@ namespace Microsoft.DotNet.Cli.MSBuild.Tests public class GivenDotnetBuildInvocation : IClassFixture { string[] ExpectedPrefix = ["-maxcpucount", "--verbosity:m", "-tlp:default=auto", "-nologo"]; - public static string[] RestoreExpectedPrefixForImplicitRestore = [..RestoringCommand.RestoreOptimizationProperties.Select(kvp => $"--restoreProperty:{kvp.Key}={kvp.Value}")]; - public static string[] RestoreExpectedPrefixForSeparateRestore = [..RestoringCommand.RestoreOptimizationProperties.Select(kvp => $"--property:{kvp.Key}={kvp.Value}")]; + public static string[] RestoreExpectedPrefixForImplicitRestore = [.. RestoringCommand.RestoreOptimizationProperties.Select(kvp => $"--restoreProperty:{kvp.Key}={kvp.Value}")]; + public static string[] RestoreExpectedPrefixForSeparateRestore = [.. RestoringCommand.RestoreOptimizationProperties.Select(kvp => $"--property:{kvp.Key}={kvp.Value}")]; const string NugetInteractiveProperty = "--property:NuGetInteractive=false"; @@ -118,5 +120,47 @@ public void MsbuildInvocationIsCorrectForSeparateRestore( .BeEquivalentTo([.. ExpectedPrefix, "-consoleloggerparameters:Summary", NugetInteractiveProperty, .. expectedAdditionalArgs]); }); } + + [Theory] + [MemberData(memberName: nameof(TelemetryCommonPropertiesTests.LLMTelemetryTestCases), MemberType =typeof(TelemetryCommonPropertiesTests))] + public void WhenLLMIsDetectedTLLiveUpdateIsDisabled(Dictionary? llmEnvVarsToSet, string? expectedLLMName) + { + CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () => + { + try + { + // Set environment variables to simulate LLM environment + if (llmEnvVarsToSet is not null) + { + foreach (var (key, value) in llmEnvVarsToSet) + { + Environment.SetEnvironmentVariable(key, value); + } + } + + var command = (RestoringCommand)BuildCommand.FromArgs([]); + + if (expectedLLMName is not null) + { + command.GetArgumentTokensToMSBuild().Should().Contain(Constants.TerminalLogger_DisableNodeDisplay); + } + else + { + command.GetArgumentTokensToMSBuild().Should().NotContain(Constants.TerminalLogger_DisableNodeDisplay); + } + } + finally + { + // Clear the environment variables after the test + if (llmEnvVarsToSet is not null) + { + foreach (var (key, value) in llmEnvVarsToSet) + { + Environment.SetEnvironmentVariable(key, null); + } + } + } + }); + } } } diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenMSBuildLogger.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenMSBuildLogger.cs index dc0ca30e12f8..12a75b9493ee 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenMSBuildLogger.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenMSBuildLogger.cs @@ -118,5 +118,101 @@ public void ItCanSendProperties() fakeTelemetry.LogEntry.Properties.Should().BeEquivalentTo(telemetryEventArgs.Properties); } + + [Fact] + public void ItAggregatesEvents() + { + var fakeTelemetry = new FakeTelemetry(); + fakeTelemetry.Enabled = true; + var logger = new MSBuildLogger(fakeTelemetry); + + var event1 = new TelemetryEventArgs + { + EventName = MSBuildLogger.TaskFactoryTelemetryAggregatedEventName, + Properties = new Dictionary + { + { "AssemblyTaskFactoryTasksExecutedCount", "2" }, + { "RoslynCodeTaskFactoryTasksExecutedCount", "1" } + } + }; + + var event2 = new TelemetryEventArgs + { + EventName = MSBuildLogger.TaskFactoryTelemetryAggregatedEventName, + Properties = new Dictionary + { + { "AssemblyTaskFactoryTasksExecutedCount", "3" }, + { "CustomTaskFactoryTasksExecutedCount", "2" } + } + }; + + var event3 = new TelemetryEventArgs + { + EventName = MSBuildLogger.TasksTelemetryAggregatedEventName, + Properties = new Dictionary + { + { "TasksExecutedCount", "3" }, + { "TaskHostTasksExecutedCount", "2" } + } + }; + + var event4 = new TelemetryEventArgs + { + EventName = MSBuildLogger.TasksTelemetryAggregatedEventName, + Properties = new Dictionary + { + { "TasksExecutedCount", "5" } + } + }; + + logger.AggregateEvent(event1); + logger.AggregateEvent(event2); + logger.AggregateEvent(event3); + logger.AggregateEvent(event4); + + logger.SendAggregatedEventsOnBuildFinished(fakeTelemetry); + + fakeTelemetry.LogEntries.Should().HaveCount(2); + + var taskFactoryEntry = fakeTelemetry.LogEntries.FirstOrDefault(e => e.EventName == $"msbuild/{MSBuildLogger.TaskFactoryTelemetryAggregatedEventName}"); + taskFactoryEntry.Should().NotBeNull(); + taskFactoryEntry.Properties["AssemblyTaskFactoryTasksExecutedCount"].Should().Be("5"); // 2 + 3 + taskFactoryEntry.Properties["RoslynCodeTaskFactoryTasksExecutedCount"].Should().Be("1"); // 1 + 0 + taskFactoryEntry.Properties["CustomTaskFactoryTasksExecutedCount"].Should().Be("2"); // 0 + 2 + + var tasksEntry = fakeTelemetry.LogEntries.FirstOrDefault(e => e.EventName == $"msbuild/{MSBuildLogger.TasksTelemetryAggregatedEventName}"); + tasksEntry.Should().NotBeNull(); + tasksEntry.Properties["TasksExecutedCount"].Should().Be("8"); // 3 + 5 + tasksEntry.Properties["TaskHostTasksExecutedCount"].Should().Be("2"); // 2 + 0 + } + + [Fact] + public void ItIgnoresNonIntegerPropertiesDuringAggregation() + { + var fakeTelemetry = new FakeTelemetry(); + fakeTelemetry.Enabled = true; + var logger = new MSBuildLogger(fakeTelemetry); + + var eventArgs = new TelemetryEventArgs + { + EventName = MSBuildLogger.TaskFactoryTelemetryAggregatedEventName, + Properties = new Dictionary + { + { "AssemblyTaskFactoryTasksExecutedCount", "3" }, + { "InvalidProperty", "not-a-number" }, + { "InvalidProperty2", "1.234" }, + } + }; + + logger.AggregateEvent(eventArgs); + + logger.SendAggregatedEventsOnBuildFinished(fakeTelemetry); + + fakeTelemetry.LogEntry.Should().NotBeNull(); + fakeTelemetry.LogEntry.EventName.Should().Be($"msbuild/{MSBuildLogger.TaskFactoryTelemetryAggregatedEventName}"); + fakeTelemetry.LogEntry.Properties["AssemblyTaskFactoryTasksExecutedCount"].Should().Be("3"); + fakeTelemetry.LogEntry.Properties.Should().NotContainKey("InvalidProperty"); + fakeTelemetry.LogEntry.Properties.Should().NotContainKey("InvalidProperty2"); + } } } diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index cb7fd4331429..72a600549d62 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Immutable; using System.Security; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.Text; @@ -1153,6 +1154,50 @@ public void Directives_EmptyName( expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, CliCommandStrings.MissingDirectiveName, directive)); } + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Directives_EmptyValue(string value) + { + VerifyConversion( + inputCSharp: $""" + #:property TargetFramework={value} + #:property Prop1={value} + #:sdk First@{value} + #:sdk Second@{value} + #:package P1@{value} + """, + expectedProject: """ + + + + + + Exe + enable + enable + true + true + + + + + + + + + + + """, + expectedCSharp: ""); + + VerifyConversionThrows( + inputCSharp: $""" + #:project{value} + """, + expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, CliCommandStrings.MissingDirectiveName, "project")); + } + [Fact] public void Directives_MissingPropertyValue() { @@ -1168,10 +1213,10 @@ public void Directives_InvalidPropertyName() { VerifyConversionThrows( inputCSharp: """ - #:property Name"=Value + #:property 123Name=Value """, expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, CliCommandStrings.PropertyDirectiveInvalidName, """ - The '"' character, hexadecimal value 0x22, cannot be included in a name. + Name cannot begin with the '1' character, hexadecimal value 0x31. """)); } @@ -1198,11 +1243,14 @@ public void Directives_Escaping() VerifyConversion( inputCSharp: """ #:property Prop= - #:sdk @="<>test - #:package @="<>test + #:sdk @="<>te'st + #:package @="<>te'st + #:property Pro'p=Single' + #:property Prop2=\"Value\" + #:property Prop3='Value' """, expectedProject: $""" - + Exe @@ -1212,16 +1260,29 @@ public void Directives_Escaping() true true <test"> + \"Value\" + 'Value' - + """, - expectedCSharp: ""); + expectedCSharp: """ + #:property Pro'p=Single' + + """, + expectedErrors: + [ + (1, CliCommandStrings.QuoteInDirective), + (2, CliCommandStrings.QuoteInDirective), + (3, CliCommandStrings.QuoteInDirective), + (4, string.Format(CliCommandStrings.PropertyDirectiveInvalidName, "The ''' character, hexadecimal value 0x27, cannot be included in a name.")), + (5, CliCommandStrings.QuoteInDirective), + ]); } [Fact] @@ -1257,7 +1318,11 @@ public void Directives_Whitespace() # ! /test #! /program x # :property Name=Value - """); + """, + expectedErrors: + [ + (3, CliCommandStrings.QuoteInDirective), + ]); } [Fact] @@ -1548,10 +1613,15 @@ public void Directives_VersionedSdkFirst() """); } - private static void Convert(string inputCSharp, out string actualProject, out string? actualCSharp, bool force, string? filePath) + private const string programPath = "/app/Program.cs"; + + private static void Convert(string inputCSharp, out string actualProject, out string? actualCSharp, bool force, string? filePath, + bool collectDiagnostics, out ImmutableArray.Builder? actualDiagnostics) { - var sourceFile = new SourceFile(filePath ?? "/app/Program.cs", SourceText.From(inputCSharp, Encoding.UTF8)); - var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !force, DiagnosticBag.ThrowOnFirst()); + var sourceFile = new SourceFile(filePath ?? programPath, SourceText.From(inputCSharp, Encoding.UTF8)); + actualDiagnostics = null; + var diagnosticBag = collectDiagnostics ? DiagnosticBag.Collect(out actualDiagnostics) : DiagnosticBag.ThrowOnFirst(); + var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag); var projectWriter = new StringWriter(); VirtualProjectBuildingCommand.WriteProjectFile(projectWriter, directives, isVirtualProject: false); actualProject = projectWriter.ToString(); @@ -1561,25 +1631,43 @@ private static void Convert(string inputCSharp, out string actualProject, out st /// /// means the conversion should not touch the C# content. /// - private static void VerifyConversion(string inputCSharp, string expectedProject, string? expectedCSharp, bool force = false, string? filePath = null) + private static void VerifyConversion(string inputCSharp, string expectedProject, string? expectedCSharp, bool force = false, string? filePath = null, + IEnumerable<(int LineNumber, string Message)>? expectedErrors = null) { - Convert(inputCSharp, out var actualProject, out var actualCSharp, force: force, filePath: filePath); + Convert(inputCSharp, out var actualProject, out var actualCSharp, force: force, filePath: filePath, + collectDiagnostics: expectedErrors != null, out var actualDiagnostics); actualProject.Should().Be(expectedProject); actualCSharp.Should().Be(expectedCSharp); + VerifyErrors(actualDiagnostics, expectedErrors); } private static void VerifyConversionThrows(string inputCSharp, string expectedWildcardPattern) { - var convert = () => Convert(inputCSharp, out _, out _, force: false, filePath: null); + var convert = () => Convert(inputCSharp, out _, out _, force: false, filePath: null, collectDiagnostics: false, out _); convert.Should().Throw().WithMessage(expectedWildcardPattern); } private static void VerifyDirectiveConversionErrors(string inputCSharp, IEnumerable<(int LineNumber, string Message)> expectedErrors) { - var programPath = "/app/Program.cs"; var sourceFile = new SourceFile(programPath, SourceText.From(inputCSharp, Encoding.UTF8)); VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics)); - Assert.All(diagnostics, d => { Assert.Equal(programPath, d.Location.Path); }); - diagnostics.Select(d => (d.Location.Span.Start.Line + 1, d.Message)).Should().BeEquivalentTo(expectedErrors); + VerifyErrors(diagnostics, expectedErrors); + } + + private static void VerifyErrors(ImmutableArray.Builder? actual, IEnumerable<(int LineNumber, string Message)>? expected) + { + if (actual is null) + { + Assert.Null(expected); + } + else if (expected is null) + { + Assert.Null(actual); + } + else + { + Assert.All(actual, d => { Assert.Equal(programPath, d.Location.Path); }); + actual.Select(d => (d.Location.Span.Start.Line + 1, d.Message)).Should().BeEquivalentTo(expected); + } } } diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs index ea2480b032f4..371b29d7f314 100644 --- a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs @@ -347,6 +347,45 @@ public void GroupWithoutSpace() """)); } + /// + /// New package directive should be sorted into the correct location in the package group. + /// + [Fact] + public void Sort() + { + Verify( + """ + #:property X=Y + #:package B@C + #:package X@Y + #:project D + #:package E + + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "Test", Version = "1.0.0" }), + """ + #:property X=Y + #:package B@C + #:package Test@1.0.0 + #:package X@Y + #:project D + #:package E + + Console.WriteLine(); + """), + (static editor => editor.Remove(editor.Directives[2]), + """ + #:property X=Y + #:package B@C + #:package X@Y + #:project D + #:package E + + Console.WriteLine(); + """)); + } + [Fact] public void OtherDirectives() { @@ -371,6 +410,33 @@ public void OtherDirectives() """)); } + /// + /// Shebang directive should always stay first. + /// + [Fact] + public void Shebang() + { + Verify( + """ + #!/test + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }), + """ + #!/test + + #:package MyPackage@1.0.0 + + Console.WriteLine(); + """), + (static editor => editor.Remove(editor.Directives[1]), + """ + #!/test + + Console.WriteLine(); + """)); + } + [Fact] public void AfterTokens() { diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 66a56562076e..3f0b93c776ea 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -878,6 +878,128 @@ public void DirectoryBuildProps() .And.HaveStdOut("Hello from TestName"); } + /// + /// Overriding default (implicit) properties of file-based apps via implicit build files. + /// + [Fact] + public void DefaultProps_DirectoryBuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hi"); + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + disable + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + + /// + /// Overriding default (implicit) properties of file-based apps from custom SDKs. + /// + [Fact] + public void DefaultProps_CustomSdk() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var sdkDir = Path.Join(testInstance.Path, "MySdk"); + Directory.CreateDirectory(sdkDir); + File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """ + + + disable + + + """); + File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """ + + """); + File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + MSBuildSdk + false + + + + + + """); + + new DotnetCommand(Log, "pack") + .WithWorkingDirectory(sdkDir) + .Execute() + .Should().Pass(); + + var appDir = Path.Join(testInstance.Path, "app"); + Directory.CreateDirectory(appDir); + File.WriteAllText(Path.Join(appDir, "NuGet.config"), $""" + + + + + + + """); + File.WriteAllText(Path.Join(appDir, "Program.cs"), """ + #:sdk Microsoft.NET.Sdk + #:sdk MySdk@1.0.0 + Console.WriteLine("Hi"); + """); + + // Use custom package cache to avoid reuse of the custom SDK packed by previous test runs. + var packagesDir = Path.Join(testInstance.Path, ".packages"); + + new DotnetCommand(Log, "run", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(Path.Join(appDir, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + [Fact] public void ComputeRunArguments_Success() { @@ -1491,6 +1613,230 @@ public void NoBuild_02() .And.HaveStdOut("Changed"); } + [Fact] + public void Build_Library() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Library + class C; + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Library_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "lib.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Library + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + class C; + """); + + // https://github.com/dotnet/sdk/issues/51077: cannot set this via `#:property` directive. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "lib.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "lib.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Library")); + } + + [Fact] + public void Build_Module() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "module.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Module + #:property ProduceReferenceAssembly=false + class C; + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "module.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "Module")); + } + + [Fact] + public void Build_WinExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "winexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=WinExe + Console.WriteLine("Hello WinExe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "winexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello WinExe"); + } + + [Fact] + public void Build_Exe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=Exe + Console.WriteLine("Hello Exe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_Exe_MultiTarget() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "exe.cs"); + File.WriteAllText(programFile, $""" + #:property OutputType=Exe + #:property PublishAot=false + #:property LangVersion=preview + #:property TargetFrameworks=netstandard2.0;{ToolsetInfo.CurrentTargetFramework} + Console.WriteLine("Hello Exe"); + """); + + // https://github.com/dotnet/sdk/issues/51077: cannot set this via `#:property` directive. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "exe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + + new DotnetCommand(Log, "run", "exe.cs", "--framework", ToolsetInfo.CurrentTargetFramework) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello Exe"); + } + + [Fact] + public void Build_AppContainerExe() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "appcontainerexe.cs"); + File.WriteAllText(programFile, """ + #:property OutputType=AppContainerExe + Console.WriteLine("Hello AppContainerExe"); + """); + + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(programFile); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run", "appcontainerexe.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, + Path.ChangeExtension(programFile, ".csproj"), + ToolsetInfo.CurrentTargetFrameworkVersion, + "AppContainerExe")); + } + [Fact] public void Publish() { @@ -3472,6 +3818,14 @@ public void Api() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3482,16 +3836,9 @@ public void Api() - Exe - enable - enable - true - true - false - true - false net11.0 preview + false $(Features);FileBasedProgram @@ -3543,6 +3890,14 @@ public void Api_Diagnostic_01() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3552,14 +3907,6 @@ public void Api_Diagnostic_01() - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - false - true false $(Features);FileBasedProgram @@ -3611,6 +3958,14 @@ public void Api_Diagnostic_02() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3620,14 +3975,6 @@ public void Api_Diagnostic_02() - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - false - true false $(Features);FileBasedProgram diff --git a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs index 8ced55de7342..08d323f2cc28 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs @@ -146,9 +146,9 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry() { // Arrange var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); - + TelemetryEventEntry.EntryPosted += handler; try @@ -172,18 +172,21 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry() var eventData = events[0]; eventData.eventName.Should().Be("run"); eventData.properties.Should().NotBeNull(); - + eventData.measurements.Should().NotBeNull(); + var props = eventData.properties!; props["app_type"].Should().Be("file_based"); props["project_id"].Should().Be("test-hash"); - props["sdk_count"].Should().Be("2"); - props["package_reference_count"].Should().Be("3"); - props["project_reference_count"].Should().Be("1"); - props["additional_properties_count"].Should().Be("2"); props["used_msbuild"].Should().Be("true"); props["used_roslyn_compiler"].Should().Be("false"); props["launch_profile_requested"].Should().Be("explicit"); props["launch_profile_is_default"].Should().Be("true"); + + var measurements = eventData.measurements!; + measurements["sdk_count"].Should().Be(2); + measurements["package_reference_count"].Should().Be(3); + measurements["project_reference_count"].Should().Be(1); + measurements["additional_properties_count"].Should().Be(2); } finally { @@ -197,9 +200,9 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry() { // Arrange var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); - + TelemetryEventEntry.EntryPosted += handler; try @@ -220,17 +223,20 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry() var eventData = events[0]; eventData.eventName.Should().Be("run"); eventData.properties.Should().NotBeNull(); - + eventData.measurements.Should().NotBeNull(); + var props = eventData.properties!; props["app_type"].Should().Be("project_based"); props["project_id"].Should().Be("project-hash"); - props["sdk_count"].Should().Be("1"); - props["package_reference_count"].Should().Be("5"); - props["project_reference_count"].Should().Be("2"); props["launch_profile_requested"].Should().Be("none"); - props.Should().NotContainKey("additional_properties_count"); props.Should().NotContainKey("used_msbuild"); props.Should().NotContainKey("used_roslyn_compiler"); + + var measurements = eventData.measurements!; + measurements["sdk_count"].Should().Be(1); + measurements["package_reference_count"].Should().Be(5); + measurements["project_reference_count"].Should().Be(2); + measurements.Should().NotContainKey("additional_properties_count"); } finally { @@ -244,9 +250,9 @@ public void TrackRunEvent_WithDefaultLaunchProfile_MarksTelemetryCorrectly() { // Arrange var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); - + TelemetryEventEntry.EntryPosted += handler; var launchSettings = new ProjectLaunchSettingsModel diff --git a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh index 0e1338dd8889..c6b13b332377 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh +++ b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh @@ -1137,7 +1137,7 @@ _testhost_package_update() { prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() - opts="--project --interactive --verbosity --help" + opts="--project --vulnerable --interactive --verbosity --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) @@ -1145,6 +1145,10 @@ _testhost_package_update() { fi case $prev in + --vulnerable) + COMPREPLY=( $(compgen -W "False True" -- "$cur") ) + return + ;; --interactive) COMPREPLY=( $(compgen -W "False True" -- "$cur") ) return diff --git a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 index e112a825b4ae..7a9a85fb4e49 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 +++ b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 @@ -48,7 +48,7 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock { [CompletionResult]::new('solution', 'solution', [CompletionResultType]::ParameterValue, ".NET modify solution file command") [CompletionResult]::new('solution', 'sln', [CompletionResultType]::ParameterValue, ".NET modify solution file command") [CompletionResult]::new('store', 'store', [CompletionResultType]::ParameterValue, "Stores the specified assemblies for the .NET Platform. By default, these will be optimized for the target runtime and framework.") - [CompletionResult]::new('test', 'test', [CompletionResultType]::ParameterValue, ".NET Test Driver") + [CompletionResult]::new('test', 'test', [CompletionResultType]::ParameterValue, ".NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test.") [CompletionResult]::new('tool', 'tool', [CompletionResultType]::ParameterValue, "Install or work with tools that extend the .NET experience.") [CompletionResult]::new('vstest', 'vstest', [CompletionResultType]::ParameterValue, "vstest") [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, ".NET CLI help utility") @@ -671,6 +671,7 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock { 'testhost;package;update' { $staticCompletions = @( [CompletionResult]::new('--project', '--project', [CompletionResultType]::ParameterName, "Path to a project or solution file, or a directory.") + [CompletionResult]::new('--vulnerable', '--vulnerable', [CompletionResultType]::ParameterName, "Upgrade packages with known vulnerabilities.") [CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, "Allows the command to stop and wait for user input or action (for example to complete authentication).") [CompletionResult]::new('--verbosity', '--verbosity', [CompletionResultType]::ParameterName, "Set the verbosity level of the command. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].") [CompletionResult]::new('--verbosity', '-v', [CompletionResultType]::ParameterName, "Set the verbosity level of the command. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].") diff --git a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh index 03a8a7c0f296..0718ee370c8f 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh +++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh @@ -669,6 +669,7 @@ _testhost() { (update) _arguments "${_arguments_options[@]}" : \ '--project=[Path to a project or solution file, or a directory.]: : ' \ + '--vulnerable=[Upgrade packages with known vulnerabilities.]: :((False\:"False" True\:"True" ))' \ '--interactive=[Allows the command to stop and wait for user input or action (for example to complete authentication).]: :((False\:"False" True\:"True" ))' \ '--verbosity=[Set the verbosity level of the command. Allowed values are q\[uiet\], m\[inimal\], n\[ormal\], d\[etailed\], and diag\[nostic\].]: :((d\:"d" detailed\:"detailed" diag\:"diag" diagnostic\:"diagnostic" m\:"m" minimal\:"minimal" n\:"n" normal\:"normal" q\:"q" quiet\:"quiet" ))' \ '-v=[Set the verbosity level of the command. Allowed values are q\[uiet\], m\[inimal\], n\[ormal\], d\[etailed\], and diag\[nostic\].]: :((d\:"d" detailed\:"detailed" diag\:"diag" diagnostic\:"diagnostic" m\:"m" minimal\:"minimal" n\:"n" normal\:"normal" q\:"q" quiet\:"quiet" ))' \ @@ -1102,7 +1103,7 @@ _testhost() { '--allow-roll-forward[Allow a .NET tool to roll forward to newer versions of the .NET runtime if the runtime it targets isn'\''t installed.]' \ '--help[Show command line help.]' \ '-h[Show command line help.]' \ - ':packageId -- Package reference in the form of a package identifier like '\''Newtonsoft.Json'\'' or package identifier and version separated by '\''@'\'' like '\''Newtonsoft.Json@13.0.3'\''.:->dotnet_dynamic_complete' \ + ':packageId -- Package reference in the form of a package identifier like '\''dotnetsay'\'' or package identifier and version separated by '\''@'\'' like '\''dotnetsay@2.1.7'\''.:->dotnet_dynamic_complete' \ && ret=0 case $state in (dotnet_dynamic_complete) @@ -1150,7 +1151,7 @@ _testhost() { '--all[Update all tools.]' \ '--help[Show command line help.]' \ '-h[Show command line help.]' \ - '::packageId -- Package reference in the form of a package identifier like '\''Newtonsoft.Json'\'' or package identifier and version separated by '\''@'\'' like '\''Newtonsoft.Json@13.0.3'\''.:->dotnet_dynamic_complete' \ + '::packageId -- Package reference in the form of a package identifier like '\''dotnetsay'\'' or package identifier and version separated by '\''@'\'' like '\''dotnetsay@2.1.7'\''.:->dotnet_dynamic_complete' \ && ret=0 case $state in (dotnet_dynamic_complete) @@ -1228,7 +1229,7 @@ _testhost() { '-v=[Set the MSBuild verbosity level. Allowed values are q\[uiet\], m\[inimal\], n\[ormal\], d\[etailed\], and diag\[nostic\].]:LEVEL:((d\:"d" detailed\:"detailed" diag\:"diag" diagnostic\:"diagnostic" m\:"m" minimal\:"minimal" n\:"n" normal\:"normal" q\:"q" quiet\:"quiet" ))' \ '--help[Show command line help.]' \ '-h[Show command line help.]' \ - ':packageId -- Package reference in the form of a package identifier like '\''Newtonsoft.Json'\'' or package identifier and version separated by '\''@'\'' like '\''Newtonsoft.Json@13.0.3'\''.:->dotnet_dynamic_complete' \ + ':packageId -- Package reference in the form of a package identifier like '\''dotnetsay'\'' or package identifier and version separated by '\''@'\'' like '\''dotnetsay@2.1.7'\''.:->dotnet_dynamic_complete' \ '*::commandArguments -- Arguments forwarded to the tool: ' \ && ret=0 case $state in @@ -1493,7 +1494,7 @@ _testhost_commands() { 'run:.NET Run Command' \ 'solution:.NET modify solution file command' \ 'store:Stores the specified assemblies for the .NET Platform. By default, these will be optimized for the target runtime and framework.' \ - 'test:.NET Test Driver' \ + 'test:.NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https\://aka.ms/dotnet-test.' \ 'tool:Install or work with tools that extend the .NET experience.' \ 'vstest:' \ 'help:.NET CLI help utility' \ diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index a77f0bf81765..4e28b92479d7 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Configurer; @@ -193,29 +191,34 @@ public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool [Theory] [MemberData(nameof(LLMTelemetryTestCases))] - public void CanDetectLLMStatusForEnvVars(Dictionary envVars, string expected) + public void CanDetectLLMStatusForEnvVars(Dictionary? envVars, string? expected) { try { - foreach (var (key, value) in envVars) - { - Environment.SetEnvironmentVariable(key, value); + if (envVars is not null){ + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, value); + } } new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected); } finally { - foreach (var (key, value) in envVars) + if (envVars is not null) { - Environment.SetEnvironmentVariable(key, null); + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, null); + } } } } - + [Theory] [InlineData("dummySessionId")] [InlineData(null)] - public void TelemetryCommonPropertiesShouldContainSessionId(string sessionId) + public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) { var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); var commonProperties = unitUnderTest.GetTelemetryCommonProperties(sessionId); @@ -225,34 +228,42 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string sessionId) } - public static IEnumerable LLMTelemetryTestCases => new List{ - new object[] { new Dictionary { { "CLAUDECODE", "1" } }, "claude" }, - new object[] { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, - new object[] { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" }, - new object[] { new Dictionary(), null }, + public static TheoryData?, string?> LLMTelemetryTestCases => new() + { + { new Dictionary { {"CLAUDECODE", "1" } }, "claude" }, + { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, + { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, + { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" }, + { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" }, + { new Dictionary { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" }, + { new Dictionary { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" }, + { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" }, + { new Dictionary { { "GEMINI_CLI", "false" } }, null }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null }, + { new Dictionary { { "AGENT_CLI", "false" } }, null }, + { new Dictionary(), null }, }; - public static IEnumerable CITelemetryTestCases => new List{ - new object[] { new Dictionary { { "TF_BUILD", "true" } }, true }, - new object[] { new Dictionary { { "GITHUB_ACTIONS", "true" } }, true }, - new object[] { new Dictionary { { "APPVEYOR", "true"} }, true }, - new object[] { new Dictionary { { "CI", "true"} }, true }, - new object[] { new Dictionary { { "TRAVIS", "true"} }, true }, - new object[] { new Dictionary { { "CIRCLECI", "true"} }, true }, - - new object[] { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" }, { "AWS_REGION", "hi" } }, true }, - new object[] { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" } }, false }, - new object[] { new Dictionary { { "BUILD_ID", "hi" }, { "BUILD_URL", "hi" } }, true }, - new object[] { new Dictionary { { "BUILD_ID", "hi" } }, false }, - new object[] { new Dictionary { { "BUILD_ID", "hi" }, { "PROJECT_ID", "hi" } }, true }, - new object[] { new Dictionary { { "BUILD_ID", "hi" } }, false }, - - new object[] { new Dictionary { { "TEAMCITY_VERSION", "hi" } }, true }, - new object[] { new Dictionary { { "TEAMCITY_VERSION", "" } }, false }, - new object[] { new Dictionary { { "JB_SPACE_API_URL", "hi" } }, true }, - new object[] { new Dictionary { { "JB_SPACE_API_URL", "" } }, false }, - - new object[] { new Dictionary { { "SomethingElse", "hi" } }, false }, + public static TheoryData, bool> CITelemetryTestCases => new() + { + { new Dictionary { { "TF_BUILD", "true" } }, true }, + { new Dictionary { { "GITHUB_ACTIONS", "true" } }, true }, + { new Dictionary { { "APPVEYOR", "true"} }, true }, + { new Dictionary { { "CI", "true"} }, true }, + { new Dictionary { { "TRAVIS", "true"} }, true }, + { new Dictionary { { "CIRCLECI", "true"} }, true }, +{ new Dictionary { { "CODEBUILD_BUILD_ID", "hi" }, { "AWS_REGION", "hi" } }, true }, + { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" } }, false }, + { new Dictionary { { "BUILD_ID", "hi" }, { "BUILD_URL", "hi" } }, true }, + { new Dictionary { { "BUILD_ID", "hi" } }, false }, + { new Dictionary { { "BUILD_ID", "hi" }, { "PROJECT_ID", "hi" } }, true }, + { new Dictionary { { "BUILD_ID", "hi" } }, false }, +{ new Dictionary { { "TEAMCITY_VERSION", "hi" } }, true }, + { new Dictionary { { "TEAMCITY_VERSION", "" } }, false }, + { new Dictionary { { "JB_SPACE_API_URL", "hi" } }, true }, + { new Dictionary { { "JB_SPACE_API_URL", "" } }, false }, +{ new Dictionary { { "SomethingElse", "hi" } }, false }, }; private class NothingCache : IUserLevelCacheWriter