From d05fe9d8e6b8ade56321dd604e38cd0301abe1a2 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 8 Apr 2026 19:01:21 +0000 Subject: [PATCH 1/3] Revert "Migrate test infrastructure from xUnit v2 to xUnit v3 (#52930)" This reverts commit a1f2e26244ad66e91094c523d0e9a87eac051cc4. --- Directory.Build.targets | 10 +++ Directory.Packages.props | 13 ++-- build/SetupHelixEnvironment.cmd | 6 -- build/SetupHelixEnvironment.sh | 6 -- build/helix-debug-entitlements.plist | 8 --- eng/Packages.props | 2 +- eng/Version.Details.props | 16 ++--- eng/Version.Details.xml | 8 +-- eng/Versions.props | 2 +- eng/dependabot/Packages.props | 2 +- ...DoNotUseCountWhenAnyCanBeUsedTests.Data.cs | 13 ++-- .../Test.Utilities/Test.Utilities.csproj | 2 +- test/Common/Program.cs | 32 +++++++++ test/Directory.Build.props | 7 +- test/Directory.Build.targets | 7 +- ...omCreateXUnitWorkItemsWithTestExclusion.cs | 15 +--- .../DebugTestOutputLogger.cs | 18 +---- ...oft.DotNet.HotReload.Test.Utilities.csproj | 1 - .../TestLogger.cs | 2 +- .../WatchSdkTest.cs | 2 +- .../WatchableApp.cs | 1 + .../DockerIsAvailableAndSupportsArchFact.cs | 11 +-- .../DockerIsAvailableAndSupportsArchTheory.cs | 11 +-- .../DockerRegistryManager.cs | 1 + .../DockerSupportsArchInlineData.cs | 20 ++---- .../DockerTestsFixture.cs | 2 - .../DockerAvailableUtils.cs | 17 +---- ...DepsJsonShouldContainVersionInformation.cs | 1 + .../CoreMSBuildAndWindowsOnlyFactAttribute.cs | 5 +- ...oreMSBuildAndWindowsOnlyTheoryAttribute.cs | 5 +- .../CoreMSBuildOnlyFactAttribute.cs | 5 +- .../CoreMSBuildOnlyTheoryAttribute.cs | 5 +- .../FullMSBuildOnlyFactAttribute.cs | 7 +- .../FullMSBuildOnlyTheoryAttribute.cs | 7 +- .../Attributes/MacOsOnlyFactAttribute.cs | 5 +- .../Attributes/PlatformSpecificFact.cs | 57 +++++++++++---- .../Attributes/PlatformSpecificTheory.cs | 41 +++++++---- .../RequiresMSBuildVersionFactAttribute.cs | 5 +- .../RequiresMSBuildVersionTheoryAttribute.cs | 5 +- .../RequiresSpecificFrameworkFactAttribute.cs | 5 +- ...equiresSpecificFrameworkTheoryAttribute.cs | 6 +- ...OnlyRequiresMSBuildVersionFactAttribute.cs | 7 +- ...lyRequiresMSBuildVersionTheoryAttribute.cs | 5 +- .../Commands/SdkCommandSpec.cs | 12 ---- .../Commands/TestCommand.cs | 9 +-- .../Microsoft.NET.TestFramework.csproj | 6 +- .../SharedTestOutputHelper.cs | 22 +----- .../StringTestLogger.cs | 12 ---- .../TestLoggerFactory.cs | 1 - ...rosoft.TemplateEngine.Cli.UnitTests.csproj | 6 +- .../Microsoft.Win32.Msi.Manual.Tests.csproj | 1 + .../Msbuild.Tests.Utilities.csproj | 1 + ...CommandLine.StaticCompletions.Tests.csproj | 3 +- test/UnitTests.proj | 3 +- .../ThirdPartyAnalyzerFormatterTests.cs | 6 +- .../XUnit/ConditionalFactAttribute.cs | 18 ++--- .../XUnit/MSBuildFactAttribute.cs | 25 ++----- .../XUnit/MSBuildFactDiscoverer.cs | 28 ++++++++ .../XUnit/MSBuildTestCase.cs | 72 +++++++++++++++++++ .../XUnit/MSBuildTheoryAttribute.cs | 28 ++------ .../XUnit/MSBuildTheoryDiscoverer.cs | 28 ++++++++ .../Diagnostic/DiagnosticFixture.cs | 2 - .../Diagnostic/XunitNuGetLogger.cs | 3 +- .../DotnetNewDetailsTest.cs | 2 - .../DotnetNewHelpTests.Approval.cs | 18 ++--- .../DotnetNewInstallTests.cs | 3 +- .../DotnetNewTestTemplatesTests.cs | 6 -- .../SharedHomeDirectory.cs | 2 +- .../TemplateDiscoveryTests.cs | 2 +- .../TemplateDiscoveryTool.cs | 2 +- .../WebProjectsTests.cs | 1 - .../dotnet-new.IntegrationTests.csproj | 6 +- .../TestUtilities/DotNetWatchTestBase.cs | 6 +- ...MSBuildFixture.cs => ModuleInitializer.cs} | 12 ++-- .../Package/Add/GivenDotnetPackageAdd.cs | 9 +-- .../Run/GivenDotnetRunIsInterrupted.cs | 4 -- ...enDotnetTestBuildsAndRunsTestfromCsproj.cs | 4 +- test/dotnet.Tests/dotnet.Tests.csproj | 6 +- test/xunit-runner/XUnitRunner.targets | 24 +------ test/xunit.runner.json | 2 +- 80 files changed, 378 insertions(+), 423 deletions(-) delete mode 100644 build/helix-debug-entitlements.plist create mode 100644 test/Common/Program.cs create mode 100644 test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs create mode 100644 test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs create mode 100644 test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs rename test/dotnet-watch.Tests/TestUtilities/{MSBuildFixture.cs => ModuleInitializer.cs} (84%) diff --git a/Directory.Build.targets b/Directory.Build.targets index 0ef1d2edf1c4..057e70d4eb7d 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -25,6 +25,16 @@ + + + + + false + false + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 26224a9a7e62..de1f7b9b33cf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,7 +58,7 @@ - + @@ -79,11 +79,11 @@ - + - + - + @@ -148,9 +148,10 @@ + - - + + - - $(NoWarn);NU5125;NU5123;xUnit1051 + $(NoWarn);NU5125;NU5123 - - false + false embedded diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 28bd22c5cdb6..edd1cbc3f09a 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -5,8 +5,6 @@ - XUnitV3 - Exe true @@ -19,6 +17,11 @@ + + + + + diff --git a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs index c8bfa9ef0dc4..a3e8eb3e64e4 100644 --- a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs +++ b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs @@ -36,11 +36,6 @@ public class SDKCustomCreateXUnitWorkItemsWithTestExclusion : Build.Utilities.Ta [Required] public bool IsPosixShell { get; set; } - /// - /// The runtime identifier of the target Helix queue (e.g. osx-arm64, linux-x64). - /// - public string TargetRid { get; set; } = ""; - /// /// Optional timeout for all created workitems /// Defaults to 300s @@ -163,15 +158,7 @@ private async Task ExecuteAsync() var testFilter = string.IsNullOrEmpty(assemblyPartitionInfo.ClassListArgumentString) ? "" : $"--filter \"{assemblyPartitionInfo.ClassListArgumentString}\""; - // xUnit v3 tests run out-of-process: the VSTest adapter launches the AppHost executable. - // On POSIX, the execute bit is lost when the Helix SDK packages the payload as a zip archive, - // so we need to restore it before running. - string exeName = Path.GetFileNameWithoutExtension(assemblyName); - string chmodPrefix = IsPosixShell ? $"chmod +x {exeName} && " : ""; - // On macOS, ad-hoc sign the test exe with get-task-allow entitlement so createdump can attach via task_for_pid for crash dumps. - string codesignPrefix = IsPosixShell && TargetRid.StartsWith("osx") ? $"codesign -s - -f --entitlements $HELIX_CORRELATION_PAYLOAD/t/helix-debug-entitlements.plist {exeName} && " : ""; - - string command = $"{chmodPrefix}{codesignPrefix}{driver} test {assemblyName} -e HELIX_WORK_ITEM_TIMEOUT={timeout} {testExecutionDirectory} {msbuildAdditionalSdkResolverFolder} " + + string command = $"{driver} test {assemblyName} -e HELIX_WORK_ITEM_TIMEOUT={timeout} {testExecutionDirectory} {msbuildAdditionalSdkResolverFolder} " + $"{(XUnitArguments != null ? " " + XUnitArguments : "")} --results-directory .{Path.DirectorySeparatorChar} --logger trx --logger \"console;verbosity=detailed\" --blame-hang --blame-hang-timeout 60m {testFilter} {enableDiagLogging} {arguments}"; Log.LogMessage($"Creating work item with properties Identity: {assemblyName}, PayloadDirectory: {publishDirectory}, Command: {command}"); diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/DebugTestOutputLogger.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/DebugTestOutputLogger.cs index dd5cd0d09508..411cbb360967 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/DebugTestOutputLogger.cs +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/DebugTestOutputLogger.cs @@ -3,35 +3,19 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watch.UnitTests; public class DebugTestOutputLogger(ITestOutputHelper logger) : ITestOutputHelper { - private readonly StringBuilder _output = new(); - public event Action? OnMessage; - public string Output => _output.ToString(); - public void Log(string message, [CallerFilePath] string? testPath = null, [CallerLineNumber] int testLine = 0) => WriteLine($"[TEST {Path.GetFileName(testPath)}:{testLine}] {message}"); - public void Write(string message) - { - _output.Append(message); - Debug.Write(message); - logger.Write(message); - OnMessage?.Invoke(message); - } - - public void Write(string format, params object[] args) - => Write(string.Format(format, args)); - public void WriteLine(string message) { - _output.AppendLine(message); Debug.WriteLine(message); try diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/Microsoft.DotNet.HotReload.Test.Utilities.csproj b/test/Microsoft.DotNet.HotReload.Test.Utilities/Microsoft.DotNet.HotReload.Test.Utilities.csproj index 113689abef9a..f267d2070886 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/Microsoft.DotNet.HotReload.Test.Utilities.csproj +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/Microsoft.DotNet.HotReload.Test.Utilities.csproj @@ -13,7 +13,6 @@ - diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/TestLogger.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/TestLogger.cs index 0b8e8b83f2d1..be7b5f747c80 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/TestLogger.cs +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/TestLogger.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; using Microsoft.Extensions.Logging; -using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watch.UnitTests; diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchSdkTest.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchSdkTest.cs index eb58e192490a..ece62d264dac 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchSdkTest.cs +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchSdkTest.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using Microsoft.NET.TestFramework; -using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watch.UnitTests; diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs index 8fa474f5caac..2dacad1cb33a 100644 --- a/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs +++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/WatchableApp.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.TestFramework; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Watch.UnitTests { diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs index 404b99624234..ae0e76f3c153 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchFact.cs @@ -1,18 +1,11 @@ -// 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.Runtime.CompilerServices; - namespace Microsoft.NET.Build.Containers.IntegrationTests; public class DockerIsAvailableAndSupportsArchFactAttribute : FactAttribute { - public DockerIsAvailableAndSupportsArchFactAttribute( - string arch, - bool checkContainerdStoreAvailability = false, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public DockerIsAvailableAndSupportsArchFactAttribute(string arch, bool checkContainerdStoreAvailability = false) { if (!DockerSupportsArchHelper.DaemonIsAvailable) { diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs index dffff31792da..382e57604fab 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerIsAvailableAndSupportsArchTheory.cs @@ -1,18 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.Build.Containers.IntegrationTests; public class DockerIsAvailableAndSupportsArchTheoryAttribute : TheoryAttribute { - public DockerIsAvailableAndSupportsArchTheoryAttribute( - string arch, - bool checkContainerdStoreAvailability = false, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public DockerIsAvailableAndSupportsArchTheoryAttribute(string arch, bool checkContainerdStoreAvailability = false) { if (!DockerSupportsArchHelper.DaemonIsAvailable) { @@ -27,4 +20,4 @@ public DockerIsAvailableAndSupportsArchTheoryAttribute( base.Skip = $"Skipping test because Docker daemon does not support {arch}."; } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs index e5b1de42ad57..5e3de400ddc3 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -3,6 +3,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.Logging; +using Xunit.Sdk; namespace Microsoft.NET.Build.Containers.IntegrationTests; diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs index 502bc3c98e22..7944436b39c9 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs @@ -3,7 +3,7 @@ using System.Reflection; using System.Text.Json; -using Xunit.v3; +using Xunit.Sdk; namespace Microsoft.NET.Build.Containers.IntegrationTests; @@ -18,19 +18,17 @@ public DockerSupportsArchInlineData(string arch, params object[] data) _data = data; } - public override bool SupportsDiscoveryEnumeration() => true; - - public override ValueTask> GetData(MethodInfo testMethod, Xunit.Sdk.DisposalTracker disposalTracker) + public override IEnumerable GetData(MethodInfo testMethod) { if (DockerSupportsArchHelper.DaemonSupportsArch(_arch)) { - return new([ConvertDataRow(_data.Prepend(_arch).ToArray())]); + return new object[][] { _data.Prepend(_arch).ToArray() }; } else { base.Skip = $"Skipping test because Docker daemon does not support {_arch}."; } - return new(Array.Empty()); + return Array.Empty(); } } @@ -105,16 +103,6 @@ private NullLogger() { } public static NullLogger Instance { get; } = new NullLogger(); - public string Output => string.Empty; - - public void Write(string message) - { - //do nothing - } - public void Write(string format, params object[] args) - { - //do nothing - } public void WriteLine(string message) { //do nothing diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs index 5a781aee4223..c417e23c6184 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.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. -using Xunit.Sdk; - namespace Microsoft.NET.Build.Containers.IntegrationTests; public sealed class DockerTestsFixture : IDisposable diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs b/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs index 585e359f213b..b4f3c899b13c 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs @@ -1,19 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.Build.Containers.UnitTests; public class DockerAvailableTheoryAttribute : TheoryAttribute { public static string LocalRegistry => DockerCliStatus.LocalRegistry; - public DockerAvailableTheoryAttribute( - bool skipPodman = false, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public DockerAvailableTheoryAttribute(bool skipPodman = false) { if (!DockerCliStatus.IsAvailable) { @@ -31,12 +25,7 @@ public class DockerAvailableFactAttribute : FactAttribute { public static string LocalRegistry => DockerCliStatus.LocalRegistry; - public DockerAvailableFactAttribute( - bool skipPodman = false, - bool checkContainerdStoreAvailability = false, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public DockerAvailableFactAttribute(bool skipPodman = false, bool checkContainerdStoreAvailability = false) { if (!DockerCliStatus.IsAvailable) { @@ -49,7 +38,7 @@ public DockerAvailableFactAttribute( else if (skipPodman && DockerCliStatus.Command == DockerCli.PodmanCommand) { base.Skip = $"Skipping test with {DockerCliStatus.Command} cli."; - } + } } } diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatAPublishedDepsJsonShouldContainVersionInformation.cs b/test/Microsoft.NET.Publish.Tests/GivenThatAPublishedDepsJsonShouldContainVersionInformation.cs index 534429acbec9..f44f135cc36f 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatAPublishedDepsJsonShouldContainVersionInformation.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatAPublishedDepsJsonShouldContainVersionInformation.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Newtonsoft.Json.Linq; using NuGet.Common; using NuGet.Frameworks; diff --git a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyFactAttribute.cs index cafada6546a2..ee3b0b7fac1b 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyFactAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class CoreMSBuildAndWindowsOnlyFactAttribute : FactAttribute { - public CoreMSBuildAndWindowsOnlyFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public CoreMSBuildAndWindowsOnlyFactAttribute() { if (SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyTheoryAttribute.cs index 7eef533ff5a4..033d688f05a6 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildAndWindowsOnlyTheoryAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class CoreMSBuildAndWindowsOnlyTheoryAttribute : TheoryAttribute { - public CoreMSBuildAndWindowsOnlyTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public CoreMSBuildAndWindowsOnlyTheoryAttribute() { if (SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyFactAttribute.cs index 4ba1ef85ae9b..cd9c7efd0472 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyFactAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class CoreMSBuildOnlyFactAttribute : FactAttribute { - public CoreMSBuildOnlyFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public CoreMSBuildOnlyFactAttribute() { if (SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyTheoryAttribute.cs index 67fc95f1f41f..729d2e6848ba 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/CoreMSBuildOnlyTheoryAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class CoreMSBuildOnlyTheoryAttribute : TheoryAttribute { - public CoreMSBuildOnlyTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public CoreMSBuildOnlyTheoryAttribute() { if (SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyFactAttribute.cs index a59f0473eed7..e9ce263c7325 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyFactAttribute.cs @@ -1,14 +1,11 @@ -// 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.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class FullMSBuildOnlyFactAttribute : FactAttribute { - public FullMSBuildOnlyFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public FullMSBuildOnlyFactAttribute() { if (!SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyTheoryAttribute.cs index d4435af75e75..e1f35bf516bf 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/FullMSBuildOnlyTheoryAttribute.cs @@ -1,14 +1,11 @@ -// 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.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class FullMSBuildOnlyTheoryAttribute : TheoryAttribute { - public FullMSBuildOnlyTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public FullMSBuildOnlyTheoryAttribute() { if (!SdkTestContext.Current.ToolsetUnderTest.ShouldUseFullFrameworkMSBuild) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/MacOsOnlyFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/MacOsOnlyFactAttribute.cs index d007a9b3110d..c3eabb4b9065 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/MacOsOnlyFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/MacOsOnlyFactAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class MacOsOnlyFactAttribute : FactAttribute { - public MacOsOnlyFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public MacOsOnlyFactAttribute() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificFact.cs b/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificFact.cs index 6089d870179e..bb521f0a1ffe 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificFact.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificFact.cs @@ -1,30 +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 System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { /// /// Controls which platforms and architectures a test should run on or be skipped on. - /// specifies platforms to include (run on). - /// Optional parameters provide additional skip-based filtering. + /// The constructor parameter specifies platforms to include (run on). + /// Named properties , , and + /// provide additional filtering. /// public class PlatformSpecificFact : FactAttribute { internal const Architecture NoArchitectureFilter = (Architecture)(-1); - public PlatformSpecificFact( - TestPlatforms platforms = TestPlatforms.Any, - TestPlatforms skipPlatforms = 0, - Architecture architecture = NoArchitectureFilter, - Architecture skipArchitecture = NoArchitectureFilter, - string? skipReason = null, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + private readonly TestPlatforms _platforms; + private string? _skip; + + public PlatformSpecificFact() : this(TestPlatforms.Any) + { + } + + public PlatformSpecificFact(TestPlatforms platforms) + { + _platforms = platforms; + } + + /// + /// Platforms to skip on, even if included by the constructor parameter. + /// When is also set, both must match for the test to be skipped. + /// + public TestPlatforms SkipPlatforms { get; set; } + + /// + /// Restrict the test to run only on this process architecture. + /// Tests on other architectures are skipped. + /// + public Architecture Architecture { get; set; } = NoArchitectureFilter; + + /// + /// Architecture to skip on. When is also set, + /// both must match for the test to be skipped. When used alone, skips on the + /// specified architecture regardless of platform. + /// + public Architecture SkipArchitecture { get; set; } = NoArchitectureFilter; + + /// + /// Reason or tracking issue URL for why the test is skipped. + /// Used as the Skip message when a skip condition matches. + /// + public string? SkipReason { get; set; } + + public override string? Skip { - Skip = EvaluateSkip(platforms, skipPlatforms, architecture, skipArchitecture, skipReason); + get => _skip ?? EvaluateSkip(_platforms, SkipPlatforms, Architecture, SkipArchitecture, SkipReason); + set => _skip = value; } internal static string? EvaluateSkip( diff --git a/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificTheory.cs b/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificTheory.cs index d44e14e1115d..81a1cd634e7e 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificTheory.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/PlatformSpecificTheory.cs @@ -1,27 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { /// /// Controls which platforms and architectures a theory should run on or be skipped on. - /// See for full documentation on the filtering parameters. + /// See for full documentation on the filtering properties. /// public class PlatformSpecificTheory : TheoryAttribute { - public PlatformSpecificTheory( - TestPlatforms platforms = TestPlatforms.Any, - TestPlatforms skipPlatforms = 0, - Architecture architecture = PlatformSpecificFact.NoArchitectureFilter, - Architecture skipArchitecture = PlatformSpecificFact.NoArchitectureFilter, - string? skipReason = null, - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + private readonly TestPlatforms _platforms; + private string? _skip; + + public PlatformSpecificTheory() : this(TestPlatforms.Any) + { + } + + public PlatformSpecificTheory(TestPlatforms platforms) + { + _platforms = platforms; + } + + /// + public TestPlatforms SkipPlatforms { get; set; } + + /// + public Architecture Architecture { get; set; } = PlatformSpecificFact.NoArchitectureFilter; + + /// + public Architecture SkipArchitecture { get; set; } = PlatformSpecificFact.NoArchitectureFilter; + + /// + public string? SkipReason { get; set; } + + public override string? Skip { - Skip = PlatformSpecificFact.EvaluateSkip(platforms, skipPlatforms, architecture, skipArchitecture, skipReason); + get => _skip ?? PlatformSpecificFact.EvaluateSkip(_platforms, SkipPlatforms, Architecture, SkipArchitecture, SkipReason); + set => _skip = value; } } } diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionFactAttribute.cs index cdc9ba84f4d5..ae359363546e 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionFactAttribute.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. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class RequiresMSBuildVersionFactAttribute : FactAttribute @@ -12,8 +10,7 @@ public class RequiresMSBuildVersionFactAttribute : FactAttribute /// public string? Reason { get; set; } - public RequiresMSBuildVersionFactAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public RequiresMSBuildVersionFactAttribute(string version) { RequiresMSBuildVersionTheoryAttribute.CheckForRequiredMSBuildVersion(this, version); } diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionTheoryAttribute.cs index 23f9bd6138ac..b09480505f59 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresMSBuildVersionTheoryAttribute.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. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class RequiresMSBuildVersionTheoryAttribute : TheoryAttribute @@ -12,8 +10,7 @@ public class RequiresMSBuildVersionTheoryAttribute : TheoryAttribute /// public string? Reason { get; set; } - public RequiresMSBuildVersionTheoryAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public RequiresMSBuildVersionTheoryAttribute(string version) { CheckForRequiredMSBuildVersion(this, version); } diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs index d3a76dcd5e97..8bbd151553f6 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs @@ -3,14 +3,11 @@ #if NETCOREAPP -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class RequiresSpecificFrameworkFactAttribute : FactAttribute { - public RequiresSpecificFrameworkFactAttribute(string framework, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public RequiresSpecificFrameworkFactAttribute(string framework) { if (!EnvironmentInfo.SupportsTargetFramework(framework)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs index 95d563362313..65550cdb8879 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs @@ -1,17 +1,15 @@ -// 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. #if NETCOREAPP -using System.Runtime.CompilerServices; using Microsoft.DotNet.Tools.Test.Utilities; namespace Microsoft.NET.TestFramework { public class RequiresSpecificFrameworkTheoryAttribute : TheoryAttribute { - public RequiresSpecificFrameworkTheoryAttribute(string framework, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public RequiresSpecificFrameworkTheoryAttribute(string framework) { if (!EnvironmentInfo.SupportsTargetFramework(framework)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs index 55df869206e1..73121c780841 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.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. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class WindowsOnlyRequiresMSBuildVersionFactAttribute : FactAttribute @@ -11,9 +9,8 @@ public class WindowsOnlyRequiresMSBuildVersionFactAttribute : FactAttribute /// Gets or sets the reason for potentially skipping the test if conditions are not met. /// public string? Reason { get; set; } - - public WindowsOnlyRequiresMSBuildVersionFactAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + + public WindowsOnlyRequiresMSBuildVersionFactAttribute(string version) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs index 2b8664843cfe..f9c43590585e 100644 --- a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs +++ b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; - namespace Microsoft.NET.TestFramework { public class WindowsOnlyRequiresMSBuildVersionTheoryAttribute : TheoryAttribute { - public WindowsOnlyRequiresMSBuildVersionTheoryAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public WindowsOnlyRequiresMSBuildVersionTheoryAttribute(string version) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs index 6de64a06b733..012d4bb1d797 100644 --- a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs +++ b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs @@ -3,9 +3,6 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Utils; -#if NET -using System.Runtime.InteropServices; -#endif namespace Microsoft.NET.TestFramework.Commands { @@ -19,8 +16,6 @@ public class SdkCommandSpec public bool RedirectStandardInput { get; set; } public bool DisableOutputAndErrorRedirection { get; set; } - public bool CreateNewProcessGroup { get; set; } - private string EscapeArgs() { // Note: this doesn't handle invoking .cmd files via "cmd /c" on Windows, which probably won't be necessary here @@ -61,13 +56,6 @@ public ProcessStartInfo ToProcessStartInfo(bool doNotEscapeArguments = false) ret.WorkingDirectory = WorkingDirectory; } -#if NET - if (CreateNewProcessGroup && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - ret.CreateNewProcessGroup = true; - } -#endif - return ret; } } diff --git a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs index 499a4f993b35..f0187fed7b8b 100644 --- a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs +++ b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs @@ -18,12 +18,6 @@ public abstract class TestCommand public bool RedirectStandardInput { get; set; } public bool DisableOutputAndErrorRedirection { get; set; } - /// - /// When true, the child process is launched in a new process group so that - /// console signals (e.g. Ctrl+C) sent to it do not propagate to the test host. - /// - public bool CreateNewProcessGroup { get; set; } - // These only work via Execute(), not when using GetProcessStartInfo() public Action? CommandOutputHandler { get; set; } public Action? ProcessStartedHandler { get; set; } @@ -116,7 +110,6 @@ private SdkCommandSpec CreateCommandSpec(IEnumerable args) commandSpec.RedirectStandardInput = RedirectStandardInput; commandSpec.DisableOutputAndErrorRedirection = DisableOutputAndErrorRedirection; - commandSpec.CreateNewProcessGroup = CreateNewProcessGroup; return commandSpec; } @@ -204,7 +197,7 @@ public virtual CommandResult Execute(IEnumerable args) public static void LogCommandResult(ITestOutputHelper log, CommandResult result) { log.WriteLine($"> {result.StartInfo.FileName} {result.StartInfo.Arguments}"); - log.WriteLine(result.StdOut ?? string.Empty); + log.WriteLine(result.StdOut); if (!string.IsNullOrEmpty(result.StdErr)) { diff --git a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj index 6aea55861808..6751b7251938 100644 --- a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj +++ b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj @@ -60,13 +60,12 @@ --> - + - - + @@ -76,6 +75,7 @@ + diff --git a/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs b/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs index c7f1a510fbbd..d3591610f624 100644 --- a/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs +++ b/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Xunit.Sdk; -using Xunit.v3; namespace Microsoft.NET.TestFramework; @@ -13,38 +12,19 @@ namespace Microsoft.NET.TestFramework; public class SharedTestOutputHelper : ITestOutputHelper { private readonly IMessageSink _sink; - private readonly StringBuilder _output = new(); public SharedTestOutputHelper(IMessageSink sink) { _sink = sink; } - public string Output => _output.ToString(); - - public void Write(string message) - { - _output.Append(message); - _sink.OnMessage(new DiagnosticMessage(message)); - } - - public void Write(string format, params object[] args) - { - var formatted = string.Format(format, args); - _output.Append(formatted); - _sink.OnMessage(new DiagnosticMessage(formatted)); - } - public void WriteLine(string message) { - _output.AppendLine(message); _sink.OnMessage(new DiagnosticMessage(message)); } public void WriteLine(string format, params object[] args) { - var formatted = string.Format(format, args); - _output.AppendLine(formatted); - _sink.OnMessage(new DiagnosticMessage(formatted)); + _sink.OnMessage(new DiagnosticMessage(format, args)); } } diff --git a/test/Microsoft.NET.TestFramework/StringTestLogger.cs b/test/Microsoft.NET.TestFramework/StringTestLogger.cs index 1d08f176332b..a5323123fba4 100644 --- a/test/Microsoft.NET.TestFramework/StringTestLogger.cs +++ b/test/Microsoft.NET.TestFramework/StringTestLogger.cs @@ -7,18 +7,6 @@ public class StringTestLogger : ITestOutputHelper { StringBuilder _stringBuilder = new(); - public string Output => _stringBuilder.ToString(); - - public void Write(string message) - { - _stringBuilder.Append(message); - } - - public void Write(string format, params object[] args) - { - _stringBuilder.Append(string.Format(format, args)); - } - public void WriteLine(string message) { _stringBuilder.AppendLine(message); diff --git a/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs b/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs index 02982eb098a9..40ec82ed3041 100644 --- a/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs +++ b/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Xunit.Sdk; namespace Microsoft.NET.TestFramework { diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj b/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj index b1e1394cf095..bf6139c9af64 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj @@ -16,10 +16,10 @@ - + - - + + diff --git a/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj b/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj index c13835ee50fe..a1465b5f3026 100644 --- a/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj +++ b/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj @@ -22,5 +22,6 @@ + diff --git a/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj b/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj index 889e80b3c357..9df4924b8ce4 100644 --- a/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj +++ b/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj @@ -16,6 +16,7 @@ + diff --git a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index 349ac13587c3..b5959c334766 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -10,7 +10,8 @@ - + + diff --git a/test/UnitTests.proj b/test/UnitTests.proj index 0920f1672a93..2eef751199e5 100644 --- a/test/UnitTests.proj +++ b/test/UnitTests.proj @@ -114,7 +114,6 @@ - @@ -142,7 +141,7 @@ . $HELIX_CORRELATION_PAYLOAD/t/SetupHelixEnvironment.sh;$(HelixPreCommands) PowerShell -ExecutionPolicy ByPass "dotnet nuget locals all -l | ForEach-Object { $_.Split(' ')[1]} | Where-Object{$_ -like '*cache'} | Get-ChildItem -Recurse -File -Filter '*.dat' | Measure";$(HelixPostCommands) PowerShell -ExecutionPolicy ByPass "Get-ChildItem -Recurse -File -Filter '*hangdump.dmp' | Copy-Item -Destination $env:HELIX_WORKITEM_UPLOAD_ROOT";$(HelixPostCommands) - find "$HELIX_WORKITEM_UPLOAD_ROOT/../../.." -name '*hangdump.dmp' -print0 | xargs -0 -I@ cp @ "$HELIX_WORKITEM_UPLOAD_ROOT";$(HelixPostCommands) + find "$HELIX_WORKITEM_UPLOAD_ROOT/../../.." -name '*hangdump.dmp' -exec cp {} "$HELIX_WORKITEM_UPLOAD_ROOT" \;;$(HelixPostCommands) $(Version) $(RepoRoot)artifacts\bin\Microsoft.DotNet.MSBuildSdkResolver $(RepoRoot)artifacts\tmp\HelixStage0.tar.gz diff --git a/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs b/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs index 939463a91d90..07fae27e9946 100644 --- a/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs +++ b/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs @@ -26,7 +26,7 @@ public ThirdPartyAnalyzerFormatterTests(ITestOutputHelper output) TestOutputHelper = output; } - public async ValueTask InitializeAsync() + public async Task InitializeAsync() { var logger = new TestLogger(); @@ -52,11 +52,11 @@ public async ValueTask InitializeAsync() } } - public ValueTask DisposeAsync() + public Task DisposeAsync() { _analyzerReferencesProject = null; - return ValueTask.CompletedTask; + return Task.CompletedTask; } private IEnumerable GetAnalyzerReferences(string prefix) diff --git a/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs b/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs index c7434c6c9d1e..e887376ef1cf 100644 --- a/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs +++ b/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs @@ -1,8 +1,6 @@ -// 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.Runtime.CompilerServices; - namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit { public class ConditionalFactAttribute : FactAttribute @@ -13,7 +11,7 @@ public class ConditionalFactAttribute : FactAttribute /// skipped vs. conditionally skipped which is the entire point of this attribute. /// [Obsolete("ConditionalFact should use Reason or AlwaysSkip", error: true)] - public new string? Skip + public new string Skip { get => base.Skip; set => base.Skip = value; @@ -23,7 +21,7 @@ public class ConditionalFactAttribute : FactAttribute /// Used to unconditionally Skip a test. For the rare occasion when a conditional test needs to be /// unconditionally skipped (typically short term for a bug to be fixed). /// - public string? AlwaysSkip + public string AlwaysSkip { get => base.Skip; set => base.Skip = value; @@ -31,8 +29,7 @@ public string? AlwaysSkip public string? Reason { get; set; } - public ConditionalFactAttribute(Type[] skipConditions, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public ConditionalFactAttribute(params Type[] skipConditions) { foreach (var skipCondition in skipConditions) { @@ -54,7 +51,7 @@ public class ConditionalTheoryAttribute : TheoryAttribute /// skipped vs. conditionally skipped which is the entire point of this attribute. /// [Obsolete("ConditionalTheory should use Reason or AlwaysSkip")] - public new string? Skip + public new string Skip { get => base.Skip; set => base.Skip = value; @@ -64,7 +61,7 @@ public class ConditionalTheoryAttribute : TheoryAttribute /// Used to unconditionally Skip a test. For the rare occasion when a conditional test needs to be /// unconditionally skipped (typically short term for a bug to be fixed). /// - public string? AlwaysSkip + public string AlwaysSkip { get => base.Skip; set => base.Skip = value; @@ -72,8 +69,7 @@ public string? AlwaysSkip public string? Reason { get; set; } - public ConditionalTheoryAttribute(Type[] skipConditions, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(sourceFilePath, sourceLineNumber) + public ConditionalTheoryAttribute(params Type[] skipConditions) { foreach (var skipCondition in skipConditions) { diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs index 97caeb8011b7..384f87887586 100644 --- a/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs +++ b/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs @@ -1,34 +1,17 @@ -// 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.Reflection; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis.Tools.Workspaces; -using Xunit.v3; +using Xunit.Sdk; namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class MSBuildFactAttribute : ConditionalFactAttribute, IBeforeAfterTestAttribute + [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildFactDiscoverer", "dotnet-format.UnitTests")] + public sealed class MSBuildFactAttribute : ConditionalFactAttribute { public MSBuildFactAttribute(params Type[] skipConditions) : base(skipConditions) { } - - public MSBuildFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(Array.Empty(), sourceFilePath, sourceLineNumber) - { - } - - public void Before(MethodInfo methodUnderTest, IXunitTest test) - { - MSBuildWorkspaceLoader.Guard.Wait(); - } - - public void After(MethodInfo methodUnderTest, IXunitTest test) - { - MSBuildWorkspaceLoader.Guard.Release(); - } } } diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs new file mode 100644 index 000000000000..6262d6d78a66 --- /dev/null +++ b/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + + public sealed class MSBuildFactDiscoverer : IXunitTestCaseDiscoverer + { + private readonly FactDiscoverer _factDiscoverer; + + public MSBuildFactDiscoverer(IMessageSink diagnosticMessageSink) + { + _factDiscoverer = new FactDiscoverer(diagnosticMessageSink); + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + return _factDiscoverer + .Discover(discoveryOptions, testMethod, factAttribute) + .Select(testCase => new MSBuildTestCase(testCase)); + } + } +} diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs new file mode 100644 index 000000000000..0104480fdff9 --- /dev/null +++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs @@ -0,0 +1,72 @@ +// 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 System.Diagnostics; +using Microsoft.CodeAnalysis.Tools.Workspaces; +using Xunit.Sdk; + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + [DebuggerDisplay(@"\{ class = {TestMethod.TestClass.Class.Name}, method = {TestMethod.Method.Name}, display = {DisplayName}, skip = {SkipReason} \}")] + public sealed class MSBuildTestCase : Xunit.LongLivedMarshalByRefObject, IXunitTestCase + { + private IXunitTestCase _testCase; + + public string DisplayName => _testCase.DisplayName; + public IMethodInfo Method => _testCase.Method; + public string SkipReason => _testCase.SkipReason; + public ITestMethod TestMethod => _testCase.TestMethod; + public object[] TestMethodArguments => _testCase.TestMethodArguments; + public Dictionary> Traits => _testCase.Traits; + public string UniqueID => _testCase.UniqueID; + + public ISourceInformation SourceInformation + { + get => _testCase.SourceInformation; + set => _testCase.SourceInformation = value; + } + + public Exception InitializationException => _testCase.InitializationException; + + public int Timeout => _testCase.Timeout; + + public MSBuildTestCase(IXunitTestCase testCase) + { + _testCase = testCase ?? throw new ArgumentNullException(nameof(testCase)); + } + + [Obsolete("Called by the deserializer", error: true)] + public MSBuildTestCase() { } + + public async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + await MSBuildWorkspaceLoader.Guard.WaitAsync(); + try + { + var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource); + return await runner.RunAsync(); + } + finally + { + MSBuildWorkspaceLoader.Guard.Release(); + } + } + + public void Deserialize(IXunitSerializationInfo info) + { + _testCase = info.GetValue("InnerTestCase"); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("InnerTestCase", _testCase); + } + } +} diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs index abf7c371c024..27b9f39b4d44 100644 --- a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs +++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs @@ -1,33 +1,17 @@ -// 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.Reflection; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis.Tools.Workspaces; -using Xunit.v3; +using Xunit.Sdk; namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class MSBuildTheoryAttribute : ConditionalTheoryAttribute, IBeforeAfterTestAttribute + [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildTheoryDiscoverer", "dotnet-format.UnitTests")] + public sealed class MSBuildTheoryAttribute : ConditionalTheoryAttribute { - public MSBuildTheoryAttribute(params Type[] skipConditions) : base(skipConditions) + public MSBuildTheoryAttribute(params Type[] skipConditions) + : base(skipConditions) { } - - public MSBuildTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0) - : base(Array.Empty(), sourceFilePath, sourceLineNumber) - { - } - - public void Before(MethodInfo methodUnderTest, IXunitTest test) - { - MSBuildWorkspaceLoader.Guard.Wait(); - } - - public void After(MethodInfo methodUnderTest, IXunitTest test) - { - MSBuildWorkspaceLoader.Guard.Release(); - } } } diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs new file mode 100644 index 000000000000..22ce8042aaf7 --- /dev/null +++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + + public sealed class MSBuildTheoryDiscoverer : IXunitTestCaseDiscoverer + { + private readonly TheoryDiscoverer _theoryDiscoverer; + + public MSBuildTheoryDiscoverer(IMessageSink diagnosticMessageSink) + { + _theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink); + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + return _theoryDiscoverer + .Discover(discoveryOptions, testMethod, factAttribute) + .Select(testCase => new MSBuildTestCase(testCase)); + } + } +} diff --git a/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs b/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs index 2eab6d6d2b9f..df772d9846de 100644 --- a/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs +++ b/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.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. -using Xunit.Sdk; - namespace Microsoft.DotNet.Cli.New.IntegrationTests { public class DiagnosticFixture diff --git a/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs b/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs index af6202fb527b..b25feb42326f 100644 --- a/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs +++ b/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs @@ -2,8 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using NuGet.Common; -using Xunit.Sdk; -using DiagnosticMessage = Xunit.v3.DiagnosticMessage; +using DiagnosticMessage = Xunit.Sdk.DiagnosticMessage; namespace Microsoft.DotNet.Cli.New.IntegrationTests { diff --git a/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs b/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs index 364e95c6580c..80fa07d8d410 100644 --- a/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs +++ b/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.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. -using Xunit.Sdk; - namespace Microsoft.DotNet.Cli.New.IntegrationTests { public partial class DotnetNewDetailsTest : BaseIntegrationTest, IClassFixture diff --git a/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs b/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs index 974ef1352b55..464fc117c1c4 100644 --- a/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs +++ b/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs @@ -208,16 +208,12 @@ public Task CannotShowHelpForTemplate_FullNameMatch() [Fact] public Task CannotShowHelpForTemplate_WhenAmbiguousLanguageChoice() { - // Use a dedicated home directory to avoid conflicts with other tests that install - // templates with the same 'basic' short name. Tests are not guaranteed to execute - // in declared order. string workingDirectory = CreateTemporaryFolder(); - string homeDirectory = CreateTemporaryFolder("Home"); - InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicFSharp", _log, homeDirectory, workingDirectory); - InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicVB", _log, homeDirectory, workingDirectory); + InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicFSharp", _log, _fixture.HomeDirectory, workingDirectory); + InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicVB", _log, _fixture.HomeDirectory, workingDirectory); CommandResult commandResult = new DotnetNewCommand(_log, "basic", "--help") - .WithCustomHive(homeDirectory) + .WithCustomHive(_fixture.HomeDirectory) .WithWorkingDirectory(workingDirectory) .Execute(); @@ -397,15 +393,11 @@ public Task CanShowHelpForTemplate_ConditionalParams() [Fact] public Task CanShowHelpForTemplateWhenRequiredParamIsMissed() { - // Use a dedicated home directory to avoid conflicts with other tests that install - // templates with the same 'basic' short name. Tests are not guaranteed to execute - // in declared order. string workingDirectory = CreateTemporaryFolder(); - string homeDirectory = CreateTemporaryFolder("Home"); - InstallTestTemplate($"TemplateResolution/MissedRequiredParameter/BasicTemplate1", _log, homeDirectory, workingDirectory); + InstallTestTemplate($"TemplateResolution/MissedRequiredParameter/BasicTemplate1", _log, _fixture.HomeDirectory, workingDirectory); CommandResult commandResult = new DotnetNewCommand(_log, "basic", "--help") - .WithCustomHive(homeDirectory) + .WithCustomHive(_fixture.HomeDirectory) .WithWorkingDirectory(workingDirectory) .Execute(); diff --git a/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs b/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs index d8c4a5d28d58..c88f7e31f49b 100644 --- a/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs +++ b/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs @@ -5,8 +5,7 @@ using System.Text.RegularExpressions; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.TestHelper; -using Xunit.Sdk; -using DiagnosticMessage = Xunit.v3.DiagnosticMessage; +using DiagnosticMessage = Xunit.Sdk.DiagnosticMessage; namespace Microsoft.DotNet.Cli.New.IntegrationTests { diff --git a/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs b/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs index a61bbcc94336..2e6128de171b 100644 --- a/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs +++ b/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs @@ -48,12 +48,6 @@ public static class Languages private class NullTestOutputHelper : ITestOutputHelper { - public string Output => string.Empty; - - public void Write(string message) { } - - public void Write(string format, params object[] args) { } - public void WriteLine(string message) { } public void WriteLine(string format, params object[] args) { } diff --git a/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs b/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs index fd946bf2aa4d..e7b91db01f32 100644 --- a/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs +++ b/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs @@ -1,7 +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 Xunit.Sdk; +using SharedTestOutputHelper = Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper; namespace Microsoft.DotNet.Cli.New.IntegrationTests { diff --git a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs index c65e813bd950..a87b0fc3cbc2 100644 --- a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs +++ b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs @@ -17,7 +17,7 @@ public TemplateDiscoveryTests(ITestOutputHelper log, TemplateDiscoveryTool templ } #pragma warning disable xUnit1004 // Test methods should not be skipped - [PlatformSpecificFact(skipPlatforms: TestPlatforms.OSX, skipArchitecture: Architecture.Arm64, skipReason: "https://github.com/dotnet/sdk/issues/53569")] + [PlatformSpecificFact(SkipPlatforms = TestPlatforms.OSX, SkipArchitecture = Architecture.Arm64, SkipReason = "https://github.com/dotnet/sdk/issues/53569")] #pragma warning restore xUnit1004 // Test methods should not be skipped public async Task CanRunDiscoveryTool() { diff --git a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs index 9d2d0a86a677..48034a63a83c 100644 --- a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs +++ b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs @@ -1,7 +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 Xunit.Sdk; +using SharedTestOutputHelper = Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper; namespace Microsoft.DotNet.Cli.New.IntegrationTests { diff --git a/test/dotnet-new.IntegrationTests/WebProjectsTests.cs b/test/dotnet-new.IntegrationTests/WebProjectsTests.cs index 59cc08e55c0c..7cd274107a21 100644 --- a/test/dotnet-new.IntegrationTests/WebProjectsTests.cs +++ b/test/dotnet-new.IntegrationTests/WebProjectsTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Utils; -using Xunit.Sdk; namespace Microsoft.DotNet.Cli.New.IntegrationTests { diff --git a/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj b/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj index e87d35c501b9..06014f7dfcda 100644 --- a/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj +++ b/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj @@ -13,10 +13,10 @@ - + - - + + diff --git a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs index 00e66f1f7762..3bbbeddb34d3 100644 --- a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs +++ b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs @@ -19,10 +19,10 @@ public DotNetWatchTestBase(ITestOutputHelper logger) TestAssets = new TestAssetsManager(App.Logger); } - public ValueTask InitializeAsync() - => default; + public Task InitializeAsync() + => Task.CompletedTask; - public async ValueTask DisposeAsync() + public async Task DisposeAsync() { Log("Disposing test"); await App.DisposeAsync(); diff --git a/test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs similarity index 84% rename from test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs rename to test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs index 71a39705de97..84836479d581 100644 --- a/test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs +++ b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs @@ -2,20 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Loader; using Microsoft.Build.Locator; -[assembly: AssemblyFixture(typeof(Microsoft.DotNet.Watch.UnitTests.MSBuildFixture))] - namespace Microsoft.DotNet.Watch.UnitTests; -/// -/// Assembly fixture that registers MSBuild and sets up assembly resolution for dotnet-watch tests. -/// A fixture is preferred over a [ModuleInitializer] because it doesn't get invoked for test discovery. -/// -public class MSBuildFixture +public static class ModuleInitializer { - public MSBuildFixture() + [ModuleInitializer] + public static void Initialize() { // Ensure that we load the msbuild binaries from redist deployment. Otherwise, msbuild might use target files // that do not match the implementations of the core tasks. diff --git a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs index 102640211fe5..744f8bcd9856 100644 --- a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs +++ b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using Microsoft.DotNet.Cli.Commands; +using Xunit.Runners; namespace Microsoft.DotNet.Cli.Package.Add.Tests { @@ -378,7 +379,7 @@ public void FileBasedApp_NoVersion(string[] inputVersions, string? expectedVersi var file = Path.Join(testInstance.Path, "Program.cs"); var source = $""" - #:property RestoreAdditionalProjectSources={restoreSources} + #:property RestoreSources=$(RestoreSources);{restoreSources} Console.WriteLine(); """; File.WriteAllText(file, source); @@ -415,7 +416,7 @@ public void FileBasedApp_NoVersion_Prerelease(string[] inputVersions, string? _, var file = Path.Join(testInstance.Path, "Program.cs"); var source = $""" - #:property RestoreAdditionalProjectSources={restoreSources} + #:property RestoreSources=$(RestoreSources);{restoreSources} Console.WriteLine(); """; File.WriteAllText(file, source); @@ -636,7 +637,7 @@ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified(bool legacy var file = Path.Join(testInstance.Path, "Program.cs"); var source = $""" - #:property RestoreAdditionalProjectSources={restoreSources} + #:property RestoreSources=$(RestoreSources);{restoreSources} Console.WriteLine(); """; File.WriteAllText(file, source); @@ -686,7 +687,7 @@ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified_KeepExistin var file = Path.Join(testInstance.Path, "Program.cs"); var source = $""" - #:property RestoreAdditionalProjectSources={restoreSources} + #:property RestoreSources=$(RestoreSources);{restoreSources} #:package A Console.WriteLine(); """; diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs index 7ebdcd079eb9..a7a188e14648 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs @@ -27,10 +27,6 @@ public void ItTerminatesWinExeAppWithCloseMainWindow() var command = new DotnetCommand(Log, "run") .WithWorkingDirectory(asset.Path); - // Launch dotnet run in a new process group so that GenerateConsoleCtrlEvent - // targets only the child group and does not propagate to the test host. - command.CreateNewProcessGroup = true; - bool signaled = false; bool sawClosingGracefully = false; Process child = null; diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs index ac056bcb2fbe..6bc213d4a4e4 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs @@ -716,9 +716,7 @@ public void EnsureOutputPathEscaped(string flag) { var testProjectDirectory = CopyAndRestoreVSTestDotNetCoreTestApp([flag]); - // Use a unique subdirectory per flag to avoid conflicts between theory data rows. - // --diag creates a file, while --output and --results-directory create directories. - var pathWithComma = Path.Combine(AppContext.BaseDirectory, "a,b", flag.TrimStart('-')); + var pathWithComma = Path.Combine(AppContext.BaseDirectory, "a,b"); // Call test CommandResult result = new DotnetTestCommand(Log, disableNewOutput: true) diff --git a/test/dotnet.Tests/dotnet.Tests.csproj b/test/dotnet.Tests/dotnet.Tests.csproj index 658735a93ee6..e5655de02a0f 100644 --- a/test/dotnet.Tests/dotnet.Tests.csproj +++ b/test/dotnet.Tests/dotnet.Tests.csproj @@ -89,9 +89,9 @@ - - - + + + diff --git a/test/xunit-runner/XUnitRunner.targets b/test/xunit-runner/XUnitRunner.targets index dce3fb456e8d..03897ea63aab 100644 --- a/test/xunit-runner/XUnitRunner.targets +++ b/test/xunit-runner/XUnitRunner.targets @@ -4,20 +4,12 @@ $(SdkTargetFramework) $(SdkTargetFramework) - $(XUnitV3Version) + 2.4.1 <_SDKCustomXUnitPublishTargetsPath>$(MSBuildThisFileDirectory)XUnitPublish.targets -nocolor - - <_TestPublishRidProperties Condition="'$(TargetRid)' != ''">RuntimeIdentifier=$(TargetRid);SelfContained=false;ErrorOnDuplicatePublishOutputFiles=false - $(ArtifactsBinDir)HelixTasks\$(Configuration)\HelixTasks.dll @@ -45,7 +37,7 @@ Outputs="%(SDKCustomXUnitProject.Identity)%(SDKCustomXUnitProject.TargetFramework)%(SDKCustomXUnitProject.RuntimeTargetFramework)%(SDKCustomXUnitProject.AdditionalProperties)"> + Properties="CustomAfterMicrosoftCommonTargets=$(_SDKCustomXUnitPublishTargetsPath);%(SDKCustomXUnitProject.AdditionalProperties)"> @@ -61,16 +53,7 @@ <_CurrentRuntimeTargetFramework Condition="'$(_CurrentRuntimeTargetFramework)' == ''">$(SDKCustomXUnitRuntimeTargetFramework) <_CurrentAdditionalProperties>%(SDKCustomXUnitProject.AdditionalProperties) - - - - + @@ -94,7 +77,6 @@ diff --git a/test/xunit.runner.json b/test/xunit.runner.json index 650eda816f3b..1fca20845e33 100644 --- a/test/xunit.runner.json +++ b/test/xunit.runner.json @@ -1,5 +1,5 @@ { - "$schema": "https://xunit.net/schema/v3.1/xunit.runner.schema.json", + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "diagnosticMessages": true, "longRunningTestSeconds": 20, "showLiveOutput": true, From 6ff39717455c693a1eb32d15ec185e6aba903558 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 9 Apr 2026 02:01:16 +0000 Subject: [PATCH 2/3] Revert "Use OTel for telemetry (#53181)" This reverts commit 794fa7f2f8d6bef67839b96d785da1cbdf099054, reversing changes made to ac1f1e89d48e98aa1142b9b966e11791a5019f20. --- Directory.Build.props | 4 +- Directory.Packages.props | 8 +- documentation/general/tab-completion.md | 2 +- eng/Signing.props | 14 +- eng/Versions.props | 3 +- .../Commands/DotNetCommandDefinition.cs | 20 +- .../Microsoft.DotNet.Cli.Utils/Activities.cs | 2 + .../BuiltInCommand.cs | 2 + .../ITelemetryFilter.cs | 10 +- .../InstallerSuccessReport.cs | 9 - .../MSBuildForwardingAppWithoutLogging.cs | 5 +- .../Microsoft.DotNet.Cli.Utils.csproj | 4 + .../TelemetryEventEntry.cs | 105 ++-- .../UILanguageOverride.cs | 4 +- .../DotnetFirstTimeUseConfigurer.cs | 56 +- .../FilePath.cs | 45 +- .../Commands/create/TemplateCommand.cs | 2 + .../Help/DotnetHelpAction.cs | 44 ++ src/Cli/dotnet/CliSchema.cs | 48 +- .../ActivityContextFactory.cs | 48 -- .../DotnetToolsCommandResolver.cs | 18 +- .../LocalToolsCommandResolver.cs | 19 +- .../CommandResolution/MuxerCommandResolver.cs | 7 +- .../MuxerCommandSpecMaker.cs | 49 +- src/Cli/dotnet/CommandFactory/CommandSpec.cs | 7 +- src/Cli/dotnet/CommandParsingException.cs | 9 +- src/Cli/dotnet/Commands/Build/BuildCommand.cs | 2 +- .../Commands/Build/BuildCommandParser.cs | 1 + src/Cli/dotnet/Commands/Clean/CleanCommand.cs | 2 +- ...NetCommandFactory.cs => CommandFactory.cs} | 39 +- .../InternalReportInstallSuccessCommand.cs | 41 +- .../Commands/MSBuild/MSBuildForwardingApp.cs | 23 +- .../dotnet/Commands/MSBuild/MSBuildLogger.cs | 78 ++- .../New/BuiltInTemplatePackageProvider.cs | 33 +- .../New/MSBuildEvaluation/MSBuildEvaluator.cs | 16 +- .../dotnet/Commands/New/NewCommandParser.cs | 2 + .../Commands/New/OptionalWorkloadProvider.cs | 17 +- src/Cli/dotnet/Commands/Pack/PackCommand.cs | 4 +- .../dotnet/Commands/Pack/PackCommandParser.cs | 1 + .../Commands/Project/ProjectCommandParser.cs | 1 + .../dotnet/Commands/Publish/PublishCommand.cs | 2 +- .../dotnet/Commands/Restore/RestoreCommand.cs | 2 +- src/Cli/dotnet/Commands/Run/RunTelemetry.cs | 14 +- .../Tool/Execute/ToolExecuteCommand.cs | 2 +- .../Commands/Tool/Run/ToolRunCommand.cs | 4 +- .../Commands/Tool/ToolCommandSpecCreator.cs | 36 +- src/Cli/dotnet/DotNetCommandFactory.cs | 47 ++ .../dotnet/Extensions/ActivityExtensions.cs | 22 - .../Extensions/ParseResultExtensions.cs | 166 +++--- src/Cli/dotnet/Parser.cs | 62 +- src/Cli/dotnet/ParserOptionActions.cs | 171 ------ src/Cli/dotnet/PerformanceLogEventListener.cs | 160 +++++ src/Cli/dotnet/PerformanceLogEventSource.cs | 455 ++++++++++++++ src/Cli/dotnet/PerformanceLogManager.cs | 136 +++++ src/Cli/dotnet/Program.cs | 564 ++++++++++-------- .../AllowListToSendFirstAppliedOptions.cs | 11 +- .../Telemetry/AllowListToSendFirstArgument.cs | 19 +- ...owListToSendVerbSecondVerbFirstArgument.cs | 16 +- .../Telemetry/ExternalTelemetryProperties.cs | 12 +- .../dotnet/Telemetry/IParseResultLogRule.cs | 2 +- .../{ITelemetryClient.cs => ITelemetry.cs} | 10 +- .../PersistenceChannel/BaseStorageService.cs | 61 ++ .../PersistenceChannel/FixedSizeQueue.cs | 43 ++ .../PersistenceChannel/FlushManager.cs | 57 ++ .../PersistenceChannel/PersistenceChannel.cs | 114 ++++ .../PersistenceChannelDebugLog.cs | 34 ++ .../PersistenceTransmitter.cs | 84 +++ .../Telemetry/PersistenceChannel/Sender.cs | 336 +++++++++++ .../SnapshottingCollection.cs | 95 +++ .../SnapshottingDictionary.cs | 71 +++ .../PersistenceChannel/StorageService.cs | 352 +++++++++++ .../PersistenceChannel/StorageTransmission.cs | 127 ++++ src/Cli/dotnet/Telemetry/Telemetry.cs | 263 ++++++++ src/Cli/dotnet/Telemetry/TelemetryClient.cs | 231 ------- .../Telemetry/TelemetryCommonProperties.cs | 100 ++-- .../dotnet/Telemetry/TelemetryDiskLogger.cs | 94 --- src/Cli/dotnet/Telemetry/TelemetryFilter.cs | 199 ++++-- .../TopLevelCommandNameAndOptionToLog.cs | 19 +- src/Cli/dotnet/dotnet.csproj | 13 +- src/Common/CompileOptions.cs | 3 +- src/Common/EnvironmentVariableNames.cs | 20 +- .../TemplateLocator.cs | 7 +- .../Commands/SdkCommandSpec.cs | 11 +- .../Commands/TestCommand.cs | 12 +- test/Microsoft.NET.TestFramework/SdkTest.cs | 59 +- test/dotnet.Tests/CliSchemaTests.cs | 2 +- .../GivenALocalToolsCommandResolver.cs | 16 +- .../CommandDirectoryContextExtensions.cs | 1 + .../MSBuild/DotnetMsbuildInProcTests.cs | 14 +- .../CommandTests/MSBuild/FakeTelemetry.cs | 35 +- .../MSBuild/GivenDotnetBuildInvocation.cs | 4 +- .../GivenDotnetMSBuildBuildsProjects.cs | 10 +- .../MSBuild/GivenDotnetRestoreInvocation.cs | 3 +- .../MSBuild/GivenDotnetTestInvocation.cs | 3 +- .../MSBuild/NullCurrentSessionIdFixture.cs | 25 +- .../CommandTests/Run/RunTelemetryTests.cs | 26 +- .../GivenADotnetFirstTimeUseConfigurer.cs | 72 +++ ...netFirstTimeUseConfigurerWIthStateSetup.cs | 19 +- .../FakeRecordEventNameTelemetry.cs | 47 ++ .../GivenThatTheUserEnablesThePerfLog.cs | 81 +++ ...atTheUserIsRunningDotNetForTheFirstTime.cs | 3 +- test/dotnet.Tests/TelemetryCommandTest.cs | 416 +++++++++++++ .../TelemetryCommonPropertiesTests.cs | 313 ++++++++++ test/dotnet.Tests/TelemetryFilterTest.cs | 211 +++++++ .../FakeRecordEventNameTelemetry.cs | 31 - .../TelemetryTests/SenderTests.cs | 184 ++++++ .../TelemetryTests/StorageTests.cs | 202 +++++++ .../TelemetryTests/TelemetryClientTests.cs | 67 --- .../TelemetryTests/TelemetryCommandTest.cs | 332 ----------- .../TelemetryCommonPropertiesTests.cs | 311 ---------- .../TelemetryTests/TelemetryFilterTest.cs | 123 ---- 111 files changed, 5351 insertions(+), 2322 deletions(-) delete mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs create mode 100644 src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs delete mode 100644 src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs rename src/Cli/dotnet/Commands/{DotNetCommandFactory.cs => CommandFactory.cs} (54%) create mode 100644 src/Cli/dotnet/DotNetCommandFactory.cs delete mode 100644 src/Cli/dotnet/Extensions/ActivityExtensions.cs delete mode 100644 src/Cli/dotnet/ParserOptionActions.cs create mode 100644 src/Cli/dotnet/PerformanceLogEventListener.cs create mode 100644 src/Cli/dotnet/PerformanceLogEventSource.cs create mode 100644 src/Cli/dotnet/PerformanceLogManager.cs rename src/Cli/dotnet/Telemetry/{ITelemetryClient.cs => ITelemetry.cs} (65%) create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs create mode 100644 src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs create mode 100644 src/Cli/dotnet/Telemetry/Telemetry.cs delete mode 100644 src/Cli/dotnet/Telemetry/TelemetryClient.cs delete mode 100644 src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs create mode 100644 test/dotnet.Tests/FakeRecordEventNameTelemetry.cs create mode 100644 test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs create mode 100644 test/dotnet.Tests/TelemetryCommandTest.cs create mode 100644 test/dotnet.Tests/TelemetryCommonPropertiesTests.cs create mode 100644 test/dotnet.Tests/TelemetryFilterTest.cs delete mode 100644 test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs create mode 100644 test/dotnet.Tests/TelemetryTests/SenderTests.cs create mode 100644 test/dotnet.Tests/TelemetryTests/StorageTests.cs delete mode 100644 test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs delete mode 100644 test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs delete mode 100644 test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs delete mode 100644 test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs diff --git a/Directory.Build.props b/Directory.Build.props index e84845c5fe8b..358a629fabb2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -66,9 +66,11 @@ $(NetCurrent) net9.0 + - $(NoWarn);NU1507;NU1202;NU5039 + $(NoWarn);NU1701;NU1507;NU1202;NU5039 true false diff --git a/Directory.Packages.props b/Directory.Packages.props index de1f7b9b33cf..21d623676105 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,23 +8,19 @@ + - - + - - - - diff --git a/documentation/general/tab-completion.md b/documentation/general/tab-completion.md index 39aaa2eaad49..8a1a977d4d97 100644 --- a/documentation/general/tab-completion.md +++ b/documentation/general/tab-completion.md @@ -9,7 +9,7 @@ Input | becomes `dotnet a⇥` | `dotnet add` | `add` is the first subcommand, alphabetically. `dotnet add p⇥` | `dotnet add --help` | it matches substrings and `--help` comes first alphabetically. `dotnet add p⇥⇥` | `dotnet add package` | pressing tab a second time brings up the next suggestion. -`dotnet add package Microsoft⇥` | `dotnet add package Microsoft.AspNetCore.Http` | results are returned alphabetically. +`dotnet add package Microsoft⇥` | `dotnet add package Microsoft.ApplicationInsights.Web` | results are returned alphabetically. `dotnet remove reference ⇥` | `dotnet remove reference ..\..\src\OmniSharp.DotNet\OmniSharp.DotNet.csproj` | it is project file aware. ## How to enable it diff --git a/eng/Signing.props b/eng/Signing.props index 760095763542..fa123b40a992 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -65,22 +65,10 @@ - - - - - - - - - - - - - + diff --git a/eng/Versions.props b/eng/Versions.props index d7d8105699a9..a858ff7d865d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -47,6 +47,7 @@ 1.0.0-20230414.1 + 2.23.0 2.0.1-servicing-26011-01 2.0.3 13.0.3 @@ -59,8 +60,6 @@ 0.3.264 1.0.52 - 1.4.0 - 1.12.0 diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs index 13a6bf284167..a7357a9f75bc 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs @@ -58,14 +58,6 @@ internal sealed class DotNetCommandDefinition : RootCommand Arity = ArgumentArity.Zero }; - public readonly Option CliSchemaOption = new("--cli-schema") - { - Description = CommandDefinitionStrings.SDKSchemaCommandDefinition, - Arity = ArgumentArity.Zero, - Recursive = true, - Hidden = true, - }; - public readonly Option ListSdksOption = new("--list-sdks") { Arity = ArgumentArity.Zero @@ -76,6 +68,14 @@ internal sealed class DotNetCommandDefinition : RootCommand Arity = ArgumentArity.Zero }; + public readonly Option CliSchemaOption = new("--cli-schema") + { + Description = CommandDefinitionStrings.SDKSchemaCommandDefinition, + Arity = ArgumentArity.Zero, + Recursive = true, + Hidden = true, + }; + public readonly AddCommandDefinition AddCommand; public readonly BuildCommandDefinition BuildCommand; public readonly BuildServerCommandDefinition BuildServerCommand; @@ -121,11 +121,9 @@ public DotNetCommandDefinition() Options.Add(DiagOption); Options.Add(VersionOption); Options.Add(InfoOption); - Options.Add(CliSchemaOption); - - // Host-handled options. Only defined to be shown in help. Options.Add(ListSdksOption); Options.Add(ListRuntimesOption); + Options.Add(CliSchemaOption); Subcommands.Add(AddCommand = new()); Subcommands.Add(BuildCommand = new()); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs index 6b22d2ad94c4..23180a6b69d1 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs @@ -3,6 +3,8 @@ #if NET +using System.Diagnostics; + namespace Microsoft.DotNet.Cli.Utils; /// diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs index d8fb130f272f..4bf3d94e2773 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.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.Diagnostics; + namespace Microsoft.DotNet.Cli.Utils; /// diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs index 593edab4ef76..2e4ff73d8935 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs @@ -1,17 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.CommandLine; - namespace Microsoft.DotNet.Cli.Utils; public interface ITelemetryFilter { - IEnumerable Filter(ParseResult parseResult); - - IEnumerable Filter(ParseResultWithGlobalJsonState parseData); - - IEnumerable Filter(InstallerSuccessReport report); - - IEnumerable Filter(Exception exception); + IEnumerable Filter(object o); } diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs deleted file mode 100644 index 42d77b93ae4b..000000000000 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs +++ /dev/null @@ -1,9 +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.Cli.Utils; - -public class InstallerSuccessReport(string? exeName) -{ - public string ExeName { get; } = exeName ?? throw new ArgumentNullException(nameof(exeName)); -} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs index 1f94685c28a8..87d021eda157 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs @@ -3,6 +3,7 @@ #if NET +using System.Diagnostics; using Microsoft.DotNet.Cli.Utils.Extensions; namespace Microsoft.DotNet.Cli.Utils; @@ -15,7 +16,7 @@ internal sealed class MSBuildForwardingAppWithoutLogging public static string MSBuildVersion { - get => Build.Evaluation.ProjectCollection.DisplayVersion; + get => Microsoft.Build.Evaluation.ProjectCollection.DisplayVersion; } private const string MSBuildExeName = "MSBuild.dll"; @@ -193,7 +194,7 @@ private static string GetMSBuildExePath() MSBuildExeName); } - public static string GetMSBuildSDKsPath() + private static string GetMSBuildSDKsPath() { var envMSBuildSDKsPath = Environment.GetEnvironmentVariable("MSBuildSDKsPath"); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj index 935472e224f2..cfa596437414 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj @@ -79,6 +79,10 @@ + + + + diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs index 032f82bf49e9..772e9845b678 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs @@ -1,7 +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.CommandLine; +using System.Diagnostics; namespace Microsoft.DotNet.Cli.Utils; @@ -10,71 +10,104 @@ public static class TelemetryEventEntry public static event EventHandler? EntryPosted; public static ITelemetryFilter TelemetryFilter { get; set; } = new BlockFilter(); - public static void TrackEvent(string eventName, IDictionary? properties = null) + public static void TrackEvent( + string? eventName = null, + IDictionary? properties = null, + IDictionary? measurements = null) { - EntryPosted?.Invoke(typeof(TelemetryEventEntry), new InstrumentationEventArgs(eventName, properties)); + EntryPosted?.Invoke(typeof(TelemetryEventEntry), + new InstrumentationEventArgs(eventName, properties, measurements)); } - public static void SendFiltered(ParseResult parseResult) => - SendFiltered(TelemetryFilter.Filter(parseResult)); - - public static void SendFiltered(ParseResultWithGlobalJsonState parseData) => - SendFiltered(TelemetryFilter.Filter(parseData)); - - public static void SendFiltered(InstallerSuccessReport report) => - SendFiltered(TelemetryFilter.Filter(report)); - - public static void SendFiltered(Exception exception) => - SendFiltered(TelemetryFilter.Filter(exception)); - - private static void SendFiltered(IEnumerable entries) + public static void SendFiltered(object? o = null) { - foreach (TelemetryEntryFormat entry in entries) + if (o == null) + { + return; + } + + foreach (ApplicationInsightsEntryFormat entry in TelemetryFilter.Filter(o)) { - TrackEvent(entry.EventName, entry.Properties); + TrackEvent(entry.EventName, entry.Properties, entry.Measurements); } } - public static void Subscribe(Action?> subscriber) + public static void Subscribe(Action?, IDictionary?> subscriber) { void Handler(object? sender, InstrumentationEventArgs eventArgs) { - subscriber(eventArgs.EventName, eventArgs.Properties); + subscriber(eventArgs.EventName, eventArgs.Properties, eventArgs.Measurements); } EntryPosted += Handler; } } -public class BlockFilter : ITelemetryFilter +public sealed class PerformanceMeasurement : IDisposable { - private static readonly TelemetryEntryFormat[] s_emptyEntries = []; + private readonly Stopwatch? _timer; + private readonly Dictionary? _data; + private readonly string? _name; - public IEnumerable Filter(ParseResult parseResult) => s_emptyEntries; + public PerformanceMeasurement(Dictionary? data, string name) + { + // Measurement is a no-op if we don't have a dictionary to store the entry. + if (data == null) + { + return; + } - public IEnumerable Filter(ParseResultWithGlobalJsonState parseData) => s_emptyEntries; + _data = data; + _name = name; + _timer = Stopwatch.StartNew(); + } - public IEnumerable Filter(InstallerSuccessReport report) => s_emptyEntries; + public void Dispose() + { + if (_name is not null && _timer is not null) + { + _data?.Add(_name, _timer.Elapsed.TotalMilliseconds); + } + } +} - public IEnumerable Filter(Exception exception) => s_emptyEntries; +public class BlockFilter : ITelemetryFilter +{ + public IEnumerable Filter(object o) + { + return []; + } } -public class InstrumentationEventArgs(string eventName, IDictionary? properties = null) : EventArgs +public class InstrumentationEventArgs : EventArgs { - public string EventName { get; } = eventName; - public IDictionary? Properties { get; } = properties; + internal InstrumentationEventArgs( + string? eventName, + IDictionary? properties, + IDictionary? measurements) + { + EventName = eventName; + Properties = properties; + Measurements = measurements; + } + + public string? EventName { get; } + public IDictionary? Properties { get; } + public IDictionary? Measurements { get; } } -public class TelemetryEntryFormat(string eventName, IDictionary? properties = null) +public class ApplicationInsightsEntryFormat( + string? eventName = null, + IDictionary? properties = null, + IDictionary? measurements = null) { - public string EventName { get; } = eventName; + public string? EventName { get; } = eventName; public IDictionary? Properties { get; } = properties; + public IDictionary? Measurements { get; } = measurements; - public TelemetryEntryFormat WithAppliedToPropertiesValue(Func func) + public ApplicationInsightsEntryFormat WithAppliedToPropertiesValue(Func func) { - var appliedProperties = Properties?.ToDictionary(p => p.Key, p => (string?)func(p.Value ?? string.Empty)); - return new TelemetryEntryFormat(EventName, appliedProperties); + var appliedProperties = Properties?.ToDictionary(p => p.Key, p => (string?)func(p.Value)); + return new ApplicationInsightsEntryFormat(EventName, appliedProperties, Measurements); } } - -public record ParseResultWithGlobalJsonState(ParseResult ParseResult, string? GlobalJsonState); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs index 6f66646cfd93..32e773f1261b 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.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.Globalization; using System.Security; using Microsoft.Win32; @@ -10,7 +11,6 @@ namespace Microsoft.DotNet.Cli.Utils; internal static class UILanguageOverride { internal const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE); - private const string DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING = nameof(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING); private const string VSLANG = nameof(VSLANG); private const string PreferredUILang = nameof(PreferredUILang); // We choose UTF8 as the default encoding as opposed to specific language encodings because it supports emojis & other chars in .NET. @@ -25,7 +25,7 @@ public static void Setup() FlowOverrideToChildProcesses(language); } - if (Env.GetEnvironmentVariable(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING) != "1") + if (Env.GetEnvironmentVariable("DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING") != "1") { if ( !CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase) && diff --git a/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs b/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs index a364b4191d2d..c52628b044ca 100644 --- a/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs +++ b/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs @@ -15,6 +15,7 @@ public class DotnetFirstTimeUseConfigurer private readonly IAspNetCoreCertificateGenerator _aspNetCoreCertificateGenerator; private readonly IFileSentinel _toolPathSentinel; private readonly IEnvironmentPath _pathAdder; + private readonly Dictionary? _performanceMeasurements; private readonly bool _skipFirstTimeUseCheck; public DotnetFirstTimeUseConfigurer( @@ -25,6 +26,7 @@ public DotnetFirstTimeUseConfigurer( DotnetFirstRunConfiguration dotnetFirstRunConfiguration, IReporter reporter, IEnvironmentPath pathAdder, + Dictionary? performanceMeasurements = null, bool skipFirstTimeUseCheck = false) { _firstTimeUseNoticeSentinel = firstTimeUseNoticeSentinel; @@ -34,6 +36,7 @@ public DotnetFirstTimeUseConfigurer( _dotnetFirstRunConfiguration = dotnetFirstRunConfiguration; _reporter = reporter; _pathAdder = pathAdder ?? throw new ArgumentNullException(nameof(pathAdder)); + _performanceMeasurements ??= performanceMeasurements; _skipFirstTimeUseCheck = skipFirstTimeUseCheck; } @@ -41,45 +44,54 @@ public void Configure() { if (_dotnetFirstRunConfiguration.AddGlobalToolsToPath && !_toolPathSentinel.Exists()) { - _pathAdder.AddPackageExecutablePathToUserPath(); - _toolPathSentinel.Create(); + using (new PerformanceMeasurement(_performanceMeasurements, "AddPackageExecutablePath Time")) + { + _pathAdder.AddPackageExecutablePathToUserPath(); + _toolPathSentinel.Create(); + } } var isFirstTimeUse = !_skipFirstTimeUseCheck && !_firstTimeUseNoticeSentinel.Exists(); var canShowFirstUseMessages = isFirstTimeUse && !_dotnetFirstRunConfiguration.NoLogo; if (isFirstTimeUse) { - // Migrate the NuGet state from earlier SDKs - NuGet.Common.Migrations.MigrationRunner.Run(); - - if (canShowFirstUseMessages) + using (new PerformanceMeasurement(_performanceMeasurements, "FirstTimeUseNotice Time")) { - _reporter.WriteLine(); - string productVersion = Product.Version; - _reporter.WriteLine(string.Format(LocalizableStrings.FirstTimeMessageWelcome, ParseDotNetVersion(productVersion), productVersion)); + // Migrate the NuGet state from earlier SDKs + NuGet.Common.Migrations.MigrationRunner.Run(); - if (!_dotnetFirstRunConfiguration.TelemetryOptout) + if (canShowFirstUseMessages) { _reporter.WriteLine(); - _reporter.WriteLine(LocalizableStrings.TelemetryMessage); + string productVersion = Product.Version; + _reporter.WriteLine(string.Format(LocalizableStrings.FirstTimeMessageWelcome, ParseDotNetVersion(productVersion), productVersion)); + + if (!_dotnetFirstRunConfiguration.TelemetryOptout) + { + _reporter.WriteLine(); + _reporter.WriteLine(LocalizableStrings.TelemetryMessage); + } } - } - _firstTimeUseNoticeSentinel.CreateIfNotExists(); + _firstTimeUseNoticeSentinel.CreateIfNotExists(); + } } if (CanGenerateAspNetCertificate()) { - _aspNetCoreCertificateGenerator.GenerateAspNetCoreDevelopmentCertificate(); - _aspNetCertificateSentinel.CreateIfNotExists(); - - if (canShowFirstUseMessages) + using (new PerformanceMeasurement(_performanceMeasurements, "GenerateAspNetCertificate Time")) { - // This message is slightly misleading for (e.g.) FreeBSD, which doesn't officially - // support `dotnet dev-certs https --trust`, but the link in the message should help - // users find the right steps for their platform. - _reporter.WriteLine(); - _reporter.WriteLine(LocalizableStrings.FirstTimeMessageAspNetCertificate); + _aspNetCoreCertificateGenerator.GenerateAspNetCoreDevelopmentCertificate(); + _aspNetCertificateSentinel.CreateIfNotExists(); + + if (canShowFirstUseMessages) + { + // This message is slightly misleading for (e.g.) FreeBSD, which doesn't officially + // support `dotnet dev-certs https --trust`, but the link in the message should help + // users find the right steps for their platform. + _reporter.WriteLine(); + _reporter.WriteLine(LocalizableStrings.FirstTimeMessageAspNetCertificate); + } } } diff --git a/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs b/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs index f08c1f1a759a..d9ad822f9517 100644 --- a/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs +++ b/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs @@ -1,33 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.EnvironmentAbstractions; - -public readonly struct FilePath +namespace Microsoft.Extensions.EnvironmentAbstractions { - public string Value { get; } - - /// - /// Create FilePath to represent an absolute file path. Note: It may not exist. - /// - /// If the value is not rooted. Path.GetFullPath will be called during the constructor. - public FilePath(string value) + public struct FilePath { - if (!Path.IsPathRooted(value)) + public string Value { get; } + + /// + /// Create FilePath to represent an absolute file path. Note it may not exist. + /// + /// If the value is not rooted. Path.GetFullPath will be called during the constructor. + public FilePath(string value) { - value = Path.GetFullPath(value); - } + if (!Path.IsPathRooted(value)) + { + value = Path.GetFullPath(value); + } - Value = value; - } + Value = value; + } - public override string ToString() - { - return Value; - } + public override string ToString() + { + return Value; + } - public DirectoryPath GetDirectoryPath() - { - return new DirectoryPath(Path.GetDirectoryName(Value)!); + public DirectoryPath GetDirectoryPath() + { + return new DirectoryPath(Path.GetDirectoryName(Value)!); + } } } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs index 2aee7d6e9360..68e6c1162794 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs @@ -22,6 +22,7 @@ internal class TemplateCommand : Command private readonly TemplatePackageManager _templatePackageManager; private readonly IEngineEnvironmentSettings _environmentSettings; private readonly Command _instantiateCommand; + private readonly TemplateGroup _templateGroup; private readonly CliTemplateInfo _template; private Dictionary _templateSpecificOptions = new(); @@ -43,6 +44,7 @@ public TemplateCommand( _instantiateCommand = instantiateCommand; _environmentSettings = environmentSettings; _templatePackageManager = templatePackageManager; + _templateGroup = templateGroup; _template = template; foreach (var item in templateGroup.ShortNames.Skip(1)) { diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs new file mode 100644 index 000000000000..884867e08e2f --- /dev/null +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.CommandLine.Invocation; +using Microsoft.DotNet.Cli.Help; + +namespace Microsoft.TemplateEngine.Cli.Help; + +/// +/// Provides command line help. +/// +public sealed class DotnetHelpAction : SynchronousCommandLineAction +{ + private HelpBuilder? _builder; + + /// + /// Specifies an to be used to format help output when help is requested. + /// + public HelpBuilder Builder + { + get => _builder ??= new HelpBuilder(Console.IsOutputRedirected ? int.MaxValue : Console.WindowWidth); + set => _builder = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public override bool ClearsParseErrors => true; + + /// + public override int Invoke(ParseResult parseResult) + { + var output = parseResult.InvocationConfiguration.Output; + + var helpContext = new HelpContext( + Builder, + parseResult.CommandResult.Command, + output, + parseResult); + + Builder.Write(helpContext); + + return 0; + } +} diff --git a/src/Cli/dotnet/CliSchema.cs b/src/Cli/dotnet/CliSchema.cs index 770f33b65fab..99d039df1db2 100644 --- a/src/Cli/dotnet/CliSchema.cs +++ b/src/Cli/dotnet/CliSchema.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Text.Json.Schema; using System.Text.Json.Serialization; -using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -32,20 +31,8 @@ internal static class CliSchema DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }); - public record ArgumentDetails( - string? description, - int order, - bool hidden, - string? helpName, - string valueType, - bool hasDefaultValue, - object? defaultValue, - ArityDetails arity); - - public record ArityDetails( - int minimum, - int? maximum); - + public record ArgumentDetails(string? description, int order, bool hidden, string? helpName, string valueType, bool hasDefaultValue, object? defaultValue, ArityDetails arity); + public record ArityDetails(int minimum, int? maximum); public record OptionDetails( string? description, bool hidden, @@ -56,8 +43,8 @@ public record OptionDetails( object? defaultValue, ArityDetails arity, bool required, - bool recursive); - + bool recursive + ); public record CommandDetails( string? description, bool hidden, @@ -65,7 +52,6 @@ public record CommandDetails( Dictionary? arguments, Dictionary? options, Dictionary? subcommands); - public record RootCommandDetails( string name, string version, @@ -77,16 +63,17 @@ public record RootCommandDetails( Dictionary? subcommands ) : CommandDetails(description, hidden, aliases, arguments, options, subcommands); - public static void PrintCliSchema(ParseResult parseResult, TextWriter outputWriter, ITelemetryClient? telemetryClient) + + public static void PrintCliSchema(CommandResult commandResult, TextWriter outputWriter, ITelemetry? telemetryClient) { - var command = parseResult.CommandResult.Command; + var command = commandResult.Command; RootCommandDetails transportStructure = CreateRootCommandDetails(command); var result = JsonSerializer.Serialize(transportStructure, s_jsonContext.RootCommandDetails); outputWriter.Write(result.AsSpan()); outputWriter.Flush(); - var commandString = parseResult.GetCommandName(); - var telemetryProperties = new Dictionary { { "command", commandString } }; - telemetryClient?.TrackEvent("schema", telemetryProperties); + var commandString = CommandHierarchyAsString(commandResult); + var telemetryProperties = new Dictionary { { "command", commandString } }; + telemetryClient?.TrackEvent("schema", telemetryProperties, null); } public static object GetJsonSchema() @@ -217,6 +204,21 @@ private static RootCommandDetails CreateRootCommandDetails(Command command) argument.HasDefaultValue ? HumanizeValue(argument.GetDefaultValue()) : null, CreateArityDetails(argument.Arity) ); + + // Produces a string that represents the command call. + // For example, calling the workload install command produces `dotnet workload install`. + private static string CommandHierarchyAsString(CommandResult commandResult) + { + var commands = new List(); + var currentResult = commandResult; + while (currentResult is not null) + { + commands.Add(currentResult.Command.Name); + currentResult = currentResult.Parent as CommandResult; + } + + return string.Join(" ", commands.AsEnumerable().Reverse()); + } } [JsonSerializable(typeof(CliSchema.RootCommandDetails))] diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs deleted file mode 100644 index bd8616a1e06a..000000000000 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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 Microsoft.DotNet.Cli.Utils; -#if TARGET_WINDOWS -using OpenTelemetry; -using OpenTelemetry.Context.Propagation; -#endif - -namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution; - -public static class ActivityContextFactory -{ - public static Dictionary? MakeActivityContextEnvironment() - { - var currentActivity = Activity.Current; - if (currentActivity is null) - { - return null; - } - var activityContext = currentActivity.Context; - if (activityContext.TraceState is null && activityContext.TraceId == default && activityContext.SpanId == default) - { - return null; - } - - var environment = new Dictionary(capacity: 2); -#if TARGET_WINDOWS - var propagationContext = new PropagationContext(activityContext, Baggage.Current); - Propagators.DefaultTextMapPropagator.Inject(propagationContext, environment, WriteTraceStateIntoEnvironment); -#endif - return environment; - } - - private static void WriteTraceStateIntoEnvironment(Dictionary environment, string key, string value) - { - switch (key) - { - case "traceparent": - environment[Activities.TRACEPARENT] = value; - break; - case "tracestate": - environment[Activities.TRACESTATE] = value; - break; - } - } -} diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs index 836fde7c220d..81b98215429d 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs @@ -1,18 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution; public class DotnetToolsCommandResolver : ICommandResolver { private readonly string _dotnetToolPath; - public DotnetToolsCommandResolver(string? dotnetToolPath = null) + public DotnetToolsCommandResolver(string dotnetToolPath = null) { - _dotnetToolPath = dotnetToolPath ?? Path.Combine(AppContext.BaseDirectory, "DotnetTools"); + if (dotnetToolPath == null) + { + _dotnetToolPath = Path.Combine(AppContext.BaseDirectory, + "DotnetTools"); + } + else + { + _dotnetToolPath = dotnetToolPath; + } } - public CommandSpec? Resolve(CommandResolverArguments arguments) + public CommandSpec Resolve(CommandResolverArguments arguments) { if (string.IsNullOrEmpty(arguments.CommandName)) { @@ -33,6 +43,6 @@ public DotnetToolsCommandResolver(string? dotnetToolPath = null) return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer( dll.FullName, - arguments.CommandArguments ?? []); + arguments.CommandArguments); } } diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs index 70ba8d1e2056..ac22545eddec 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs @@ -1,27 +1,30 @@ // 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.Commands.Tool; using Microsoft.DotNet.Cli.ToolManifest; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; +using NuGet.DependencyResolver; using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution; internal class LocalToolsCommandResolver( - ToolManifestFinder? toolManifest = null, - ILocalToolsResolverCache? localToolsResolverCache = null, - IFileSystem? fileSystem = null, - string? currentWorkingDirectory = null) : ICommandResolver + ToolManifestFinder toolManifest = null, + ILocalToolsResolverCache localToolsResolverCache = null, + IFileSystem fileSystem = null, + string currentWorkingDirectory = null) : ICommandResolver { private readonly ToolManifestFinder _toolManifest = toolManifest ?? new ToolManifestFinder(new DirectoryPath(currentWorkingDirectory ?? Directory.GetCurrentDirectory())); private readonly ILocalToolsResolverCache _localToolsResolverCache = localToolsResolverCache ?? new LocalToolsResolverCache(); private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystemWrapper(); private const string LeadingDotnetPrefix = "dotnet-"; - public CommandSpec? ResolveStrict(CommandResolverArguments arguments, bool allowRollForward = false) + public CommandSpec ResolveStrict(CommandResolverArguments arguments, bool allowRollForward = false) { if (arguments == null || string.IsNullOrWhiteSpace(arguments.CommandName)) { @@ -39,7 +42,7 @@ internal class LocalToolsCommandResolver( return resolveResult; } - public CommandSpec? Resolve(CommandResolverArguments arguments) + public CommandSpec Resolve(CommandResolverArguments arguments) { if (arguments == null || string.IsNullOrWhiteSpace(arguments.CommandName)) { @@ -66,7 +69,7 @@ internal class LocalToolsCommandResolver( return GetPackageCommandSpecUsingMuxer(arguments, new ToolCommandName(arguments.CommandName)); } - private CommandSpec? GetPackageCommandSpecUsingMuxer(CommandResolverArguments arguments, + private CommandSpec GetPackageCommandSpecUsingMuxer(CommandResolverArguments arguments, ToolCommandName toolCommandName, bool allowRollForward = false) { if (!_toolManifest.TryFind(toolCommandName, out var toolManifestPackage)) @@ -90,7 +93,7 @@ internal class LocalToolsCommandResolver( } return ToolCommandSpecCreator.CreateToolCommandSpec(toolCommand.Name.Value, toolCommand.Executable.Value, toolCommand.Runner, - toolManifestPackage.RollForward || allowRollForward, arguments.CommandArguments ?? []); + toolManifestPackage.RollForward || allowRollForward, arguments.CommandArguments); } else { diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs index 92f7d50f6abd..1998ae144809 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.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. +#nullable disable + using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -8,15 +10,14 @@ namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution; public class MuxerCommandResolver : ICommandResolver { - public CommandSpec? Resolve(CommandResolverArguments commandResolverArguments) + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) { if (commandResolverArguments.CommandName == Muxer.MuxerName) { var muxer = new Muxer(); var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( commandResolverArguments.CommandArguments.OrEmptyIfNull()); - var environment = ActivityContextFactory.MakeActivityContextEnvironment(); - return new CommandSpec(muxer.MuxerPath, escapedArgs, environment); + return new CommandSpec(muxer.MuxerPath, escapedArgs); } return null; } diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs index 12f1a372e652..63a3ecae1d5b 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs @@ -1,16 +1,31 @@ // 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.Utils; namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution; internal static class MuxerCommandSpecMaker { - internal static CommandSpec CreatePackageCommandSpecUsingMuxer(string commandPath, IEnumerable commandArguments, IDictionary? environment = null) + internal static CommandSpec CreatePackageCommandSpecUsingMuxer( + string commandPath, + IEnumerable commandArguments) { var arguments = new List(); - var rollForwardArgument = commandArguments.Where(arg => arg.Equals("--allow-roll-forward", StringComparison.OrdinalIgnoreCase)); + + var muxer = new Muxer(); + + var host = muxer.MuxerPath; + + if (host == null) + { + throw new Exception(LocalizableStrings.UnableToLocateDotnetMultiplexer); + } + + var rollForwardArgument = (commandArguments ?? []).Where(arg => arg.Equals("--allow-roll-forward", StringComparison.OrdinalIgnoreCase)); + if (rollForwardArgument.Any()) { arguments.Add("--roll-forward"); @@ -18,13 +33,27 @@ internal static CommandSpec CreatePackageCommandSpecUsingMuxer(string commandPat } arguments.Add(commandPath); - var filteredCommandArgs = rollForwardArgument.Any() - ? commandArguments.Except(rollForwardArgument) - : commandArguments; - arguments.AddRange(filteredCommandArgs); - - var host = new Muxer().MuxerPath; - var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(arguments); - return new CommandSpec(host, escapedArgs, environment); + + if (commandArguments != null) + { + if (rollForwardArgument.Any()) + { + arguments.AddRange(commandArguments.Except(rollForwardArgument)); + } + else + { + arguments.AddRange(commandArguments); + } + } + return CreateCommandSpec(host, arguments); + } + + private static CommandSpec CreateCommandSpec( + string commandPath, + IEnumerable commandArguments) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments); + + return new CommandSpec(commandPath, escapedArgs); } } diff --git a/src/Cli/dotnet/CommandFactory/CommandSpec.cs b/src/Cli/dotnet/CommandFactory/CommandSpec.cs index 0b0cf47a9c56..6af14dd0b931 100644 --- a/src/Cli/dotnet/CommandFactory/CommandSpec.cs +++ b/src/Cli/dotnet/CommandFactory/CommandSpec.cs @@ -5,13 +5,16 @@ namespace Microsoft.DotNet.Cli.CommandFactory; -public class CommandSpec(string path, string? args, IDictionary? environmentVariables = null) +public class CommandSpec( + string path, + string? args, + Dictionary? environmentVariables = null) { public string Path { get; } = path; public string? Args { get; } = args; - public IDictionary EnvironmentVariables { get; } = environmentVariables ?? new Dictionary(); + public Dictionary EnvironmentVariables { get; } = environmentVariables ?? []; internal void AddEnvironmentVariablesFromProject(IProject project) { diff --git a/src/Cli/dotnet/CommandParsingException.cs b/src/Cli/dotnet/CommandParsingException.cs index 6582c7373d84..f932b55aefb6 100644 --- a/src/Cli/dotnet/CommandParsingException.cs +++ b/src/Cli/dotnet/CommandParsingException.cs @@ -1,18 +1,21 @@ // 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 System.CommandLine; namespace Microsoft.DotNet.Cli; internal class CommandParsingException : Exception { - public CommandParsingException(string message, ParseResult? parseResult = null) - : base(message) + public CommandParsingException( + string message, + ParseResult parseResult = null) : base(message) { ParseResult = parseResult; Data.Add("CLI_User_Displayed_Exception", true); } - public ParseResult? ParseResult; + public ParseResult ParseResult; } diff --git a/src/Cli/dotnet/Commands/Build/BuildCommand.cs b/src/Cli/dotnet/Commands/Build/BuildCommand.cs index b0276710516d..cde474ec73c2 100644 --- a/src/Cli/dotnet/Commands/Build/BuildCommand.cs +++ b/src/Cli/dotnet/Commands/Build/BuildCommand.cs @@ -29,7 +29,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui bool noRestore = parseResult.HasOption(definition.NoRestoreOption); - return DotNetCommandFactory.CreateVirtualOrPhysicalCommand( + return CommandFactory.CreateVirtualOrPhysicalCommand( definition, definition.SlnOrProjectOrFileArgument, createVirtualCommand: (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand( diff --git a/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs b/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs index bc1289c7ae2e..316b422c8a85 100644 --- a/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs +++ b/src/Cli/dotnet/Commands/Build/BuildCommandParser.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.CommandLine; using Microsoft.DotNet.Cli.CommandLine; namespace Microsoft.DotNet.Cli.Commands.Build; diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs index 5584a77f2461..d1772e247a4f 100644 --- a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs +++ b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs @@ -22,7 +22,7 @@ public static CommandBase FromParseResult(ParseResult result, string? msbuildPat var definition = (CleanCommandDefinition)result.CommandResult.Command; result.ShowHelpOrErrorIfAppropriate(); - return DotNetCommandFactory.CreateVirtualOrPhysicalCommand( + return CommandFactory.CreateVirtualOrPhysicalCommand( definition, definition.SlnOrProjectOrFileArgument, createVirtualCommand: static (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand( diff --git a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs b/src/Cli/dotnet/Commands/CommandFactory.cs similarity index 54% rename from src/Cli/dotnet/Commands/DotNetCommandFactory.cs rename to src/Cli/dotnet/Commands/CommandFactory.cs index ba08b4864f7d..6f50a6b2d4d7 100644 --- a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs +++ b/src/Cli/dotnet/Commands/CommandFactory.cs @@ -1,49 +1,16 @@ -// 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.CommandLine; -using System.CommandLine.Invocation; -using System.Diagnostics; -using Microsoft.DotNet.Cli.CommandFactory; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectTools; -using NuGet.Frameworks; -namespace Microsoft.DotNet.Cli; +namespace Microsoft.DotNet.Cli.Commands; -public class DotNetCommandFactory(bool alwaysRunOutOfProc = false, string? currentWorkingDirectory = null) : ICommandFactory +public static class CommandFactory { - private readonly bool _alwaysRunOutOfProc = alwaysRunOutOfProc; - private readonly string? _currentWorkingDirectory = currentWorkingDirectory; - - public ICommand Create(string commandName, IEnumerable args, NuGetFramework? framework = null, string configuration = Constants.DefaultConfiguration) - { - if (!_alwaysRunOutOfProc && TryGetBuiltInCommand(commandName, out var builtInCommand)) - { - Debug.Assert(framework == null, "BuiltInCommand doesn't support the 'framework' argument."); - Debug.Assert(configuration == Constants.DefaultConfiguration, "BuiltInCommand doesn't support the 'configuration' argument."); - - return new BuiltInCommand(commandName, args, builtInCommand); - } - - return CommandFactoryUsingResolver.CreateDotNet(commandName, args, framework, configuration, _currentWorkingDirectory); - } - - private static bool TryGetBuiltInCommand(string commandName, out Func commandFunc) - { - var command = Parser.GetBuiltInCommand(commandName); - if (command?.Action is AsynchronousCommandLineAction action) - { - commandFunc = (args) => Parser.Invoke([commandName, .. args]); - return true; - } - // No-op delegate for failure case. - commandFunc = (args) => 1; - return false; - } - internal static CommandBase CreateVirtualOrPhysicalCommand( System.CommandLine.Command commandDefinition, Argument catchAllUserInputArgument, diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs index c48ff5353e5c..c6a6d87ff9df 100644 --- a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs @@ -1,29 +1,36 @@ // 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 System.CommandLine; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; using Microsoft.DotNet.Utilities; namespace Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess; public class InternalReportInstallSuccessCommand { + internal const string TelemetrySessionIdEnvironmentVariableName = "DOTNET_CLI_TELEMETRY_SESSIONID"; + public static int Run(ParseResult parseResult) { var telemetry = new ThreadBlockingTelemetry(); ProcessInputAndSendTelemetry(parseResult, telemetry); + telemetry.Dispose(); return 0; } - public static void ProcessInputAndSendTelemetry(string[] args, ITelemetryClient telemetry) + public static void ProcessInputAndSendTelemetry(string[] args, ITelemetry telemetry) { var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", .. args]); ProcessInputAndSendTelemetry(result, telemetry); } - public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetryClient telemetry) + public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetry telemetry) { var definition = (InternalReportInstallSuccessCommandDefinition)result.CommandResult.Command; var exeName = Path.GetFileName(result.GetValue(definition.Argument)); @@ -31,25 +38,39 @@ public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetryCl var filter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); foreach (var e in filter.Filter(new InstallerSuccessReport(exeName))) { - telemetry.TrackEvent(e.EventName, e.Properties); + telemetry.TrackEvent(e.EventName, e.Properties, null); } } - internal class ThreadBlockingTelemetry : ITelemetryClient + internal class ThreadBlockingTelemetry : ITelemetry { - private readonly TelemetryClient _telemetry; + private readonly Telemetry.Telemetry telemetry; internal ThreadBlockingTelemetry() { - var sessionId = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID); - _telemetry = new TelemetryClient(sessionId); + var sessionId = + Environment.GetEnvironmentVariable(TelemetrySessionIdEnvironmentVariableName); + telemetry = new Telemetry.Telemetry(new NoOpFirstTimeUseNoticeSentinel(), sessionId, blockThreadInitialization: true); + } + public bool Enabled => telemetry.Enabled; + + public void Flush() + { } - public bool Enabled => _telemetry.Enabled; + public void Dispose() + { + telemetry.Dispose(); + } - public void TrackEvent(string eventName, IDictionary? properties) + public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements) { - _telemetry.ThreadBlockingTrackEvent(eventName, properties); + telemetry.ThreadBlockingTrackEvent(eventName, properties, measurements); } } } + +internal class InstallerSuccessReport(string exeName) +{ + public string ExeName { get; } = exeName ?? throw new ArgumentNullException(nameof(exeName)); +} diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs index 7f8364ddf11c..d6fb42ac39f0 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Reflection; using Microsoft.DotNet.Cli.Commands.Run; -using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -12,6 +11,8 @@ namespace Microsoft.DotNet.Cli.Commands.MSBuild; public class MSBuildForwardingApp : CommandBase { + internal const string TelemetrySessionIdEnvironmentVariableName = "DOTNET_CLI_TELEMETRY_SESSIONID"; + private readonly MSBuildForwardingAppWithoutLogging _forwardingAppWithoutLogging; /// @@ -19,7 +20,7 @@ public class MSBuildForwardingApp : CommandBase /// private static MSBuildArgs ConcatTelemetryLogger(MSBuildArgs msbuildArgs) { - if (TelemetryClient.CurrentSessionId != null) + if (Telemetry.Telemetry.CurrentSessionId != null) { try { @@ -54,6 +55,12 @@ public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null) _forwardingAppWithoutLogging = new MSBuildForwardingAppWithoutLogging( modifiedMSBuildArgs, msbuildPath: msbuildPath); + + // Add the performance log location to the environment of the target process. + if (PerformanceLogManager.Instance != null && !string.IsNullOrEmpty(PerformanceLogManager.Instance.CurrentLogDirectory)) + { + EnvironmentVariable(PerformanceLogManager.PerfLogDirEnvVar, PerformanceLogManager.Instance.CurrentLogDirectory); + } } public IEnumerable MSBuildArguments { get { return _forwardingAppWithoutLogging.GetAllArguments(); } } @@ -72,7 +79,7 @@ public ProcessStartInfo GetProcessStartInfo() private void InitializeRequiredEnvironmentVariables() { - EnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID, TelemetryClient.CurrentSessionId); + EnvironmentVariable(TelemetrySessionIdEnvironmentVariableName, Telemetry.Telemetry.CurrentSessionId); } /// @@ -92,13 +99,23 @@ public override int Execute() if (_forwardingAppWithoutLogging.ExecuteMSBuildOutOfProc) { ProcessStartInfo startInfo = GetProcessStartInfo(); + + PerformanceLogEventSource.Log.LogMSBuildStart(startInfo.FileName, startInfo.Arguments); exitCode = startInfo.Execute(); + PerformanceLogEventSource.Log.MSBuildStop(exitCode); } else { InitializeRequiredEnvironmentVariables(); string[] arguments = _forwardingAppWithoutLogging.GetAllArguments(); + if (PerformanceLogEventSource.Log.IsEnabled()) + { + PerformanceLogEventSource.Log.LogMSBuildStart( + _forwardingAppWithoutLogging.MSBuildPath, + ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(arguments)); + } exitCode = _forwardingAppWithoutLogging.ExecuteInProc(arguments); + PerformanceLogEventSource.Log.MSBuildStop(exitCode); } return exitCode; diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs index 608f4caf5273..bf770da1c33b 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs @@ -1,16 +1,20 @@ -// 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; using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Telemetry; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; using Microsoft.DotNet.Utilities; namespace Microsoft.DotNet.Cli.Commands.MSBuild; public sealed class MSBuildLogger : INodeLogger { - private readonly ITelemetryClient? _telemetry; + private readonly IFirstTimeUseNoticeSentinel _sentinel = + new FirstTimeUseNoticeSentinel(); + private readonly ITelemetry? _telemetry; internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval"; internal const string BuildTelemetryEventName = "build"; @@ -62,11 +66,19 @@ public MSBuildLogger() { try { - string? sessionId = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID); + string? sessionId = + Environment.GetEnvironmentVariable(MSBuildForwardingApp.TelemetrySessionIdEnvironmentVariableName); if (sessionId != null) { - _telemetry = new TelemetryClient(sessionId); + // senderCount: 0 to disable sender. + // When senders in different process running at the same + // time they will read from the same global queue and cause + // sending duplicated events. Disable sender to reduce it. + _telemetry = new Telemetry.Telemetry( + _sentinel, + sessionId, + senderCount: 0); } } catch (Exception) @@ -78,7 +90,7 @@ public MSBuildLogger() /// /// Constructor for testing purposes. /// - internal MSBuildLogger(ITelemetryClient telemetry) + internal MSBuildLogger(ITelemetry telemetry) { _telemetry = telemetry; } @@ -105,6 +117,8 @@ public void Initialize(IEventSource eventSource) { eventSource2.TelemetryLogged += OnTelemetryLogged; } + + eventSource.BuildFinished += OnBuildFinished; } eventSource.BuildFinished += OnBuildFinished; @@ -120,14 +134,14 @@ private void OnBuildFinished(object sender, BuildFinishedEventArgs e) SendAggregatedEventsOnBuildFinished(_telemetry); } - internal void SendAggregatedEventsOnBuildFinished(ITelemetryClient? 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: []); + TrackEvent(telemetry, $"msbuild/{TaskFactoryTelemetryAggregatedEventName}", taskFactoryProperties, toBeHashed: [], toBeMeasured: []); _aggregatedEvents.Remove(TaskFactoryTelemetryAggregatedEventName); } @@ -135,7 +149,7 @@ internal void SendAggregatedEventsOnBuildFinished(ITelemetryClient? telemetry) { Dictionary tasksProperties = ConvertToStringDictionary(tasksData); - TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: []); + TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: [], toBeMeasured: []); _aggregatedEvents.Remove(TasksTelemetryAggregatedEventName); } } @@ -176,7 +190,7 @@ internal void AggregateEvent(TelemetryEventArgs args) } } - internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventArgs args) + internal static void FormatAndSend(ITelemetry? telemetry, TelemetryEventArgs args) { switch (args.EventName) { @@ -185,13 +199,14 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr break; case BuildTelemetryEventName: TrackEvent(telemetry, $"msbuild/{BuildTelemetryEventName}", args.Properties, - toBeHashed: ["ProjectPath", "BuildTarget"] + toBeHashed: ["ProjectPath", "BuildTarget"], + toBeMeasured: ["BuildDurationInMilliseconds", "InnerBuildDurationInMilliseconds"] ); break; case LoggingConfigurationTelemetryEventName: TrackEvent(telemetry, $"msbuild/{LoggingConfigurationTelemetryEventName}", args.Properties, - toBeHashed: [] - ); + toBeHashed: [], + toBeMeasured: []); break; case BuildcheckAcquisitionFailureEventName: TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties, @@ -199,11 +214,14 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr ); break; case BuildcheckRunEventName: - TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties); + TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties, + toBeMeasured: ["TotalRuntimeInMilliseconds"] + ); break; case BuildcheckRuleStatsEventName: TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties, - toBeHashed: ["RuleId", "CheckFriendlyName"] + toBeHashed: ["RuleId", "CheckFriendlyName"], + toBeMeasured: ["TotalRuntimeInMilliseconds"] ); break; // Pass through events that don't need special handling @@ -222,7 +240,7 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr } } - private static void TrackEvent(ITelemetryClient? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed = null) + private static void TrackEvent(ITelemetry? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed = null, string[]? toBeMeasured = null) { if (telemetry == null || !telemetry.Enabled) { @@ -230,6 +248,7 @@ private static void TrackEvent(ITelemetryClient? telemetry, string eventName, ID } Dictionary? properties = null; + Dictionary? measurements = null; if (toBeHashed is not null) { @@ -244,7 +263,26 @@ private static void TrackEvent(ITelemetryClient? telemetry, string eventName, ID } } - telemetry?.TrackEvent(eventName, properties ?? eventProperties); + if (toBeMeasured is not null) + { + foreach (var propertyToBeMeasured in toBeMeasured) + { + if (eventProperties.TryGetValue(propertyToBeMeasured, out var value)) + { + // Lets lazy allocate in case there is tons of telemetry + properties ??= new(eventProperties); + properties.Remove(propertyToBeMeasured); + if (double.TryParse(value, CultureInfo.InvariantCulture, out double realValue)) + { + // Lets lazy allocate in case there is tons of telemetry + measurements ??= []; + measurements[propertyToBeMeasured] = realValue; + } + } + } + } + + telemetry.TrackEvent(eventName, properties ?? eventProperties, measurements); } private void OnTelemetryLogged(object sender, TelemetryEventArgs args) @@ -261,6 +299,14 @@ private void OnTelemetryLogged(object sender, TelemetryEventArgs args) public void Shutdown() { + try + { + _sentinel?.Dispose(); + } + catch (Exception) + { + // Exceptions during telemetry shouldn't cause anything else to fail + } } public LoggerVerbosity Verbosity { get; set; } diff --git a/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs b/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs index f25795528dd4..8858145a6c0a 100644 --- a/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs +++ b/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs @@ -1,7 +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 Microsoft.DotNet.Cli.Utils; +#nullable disable + using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.TemplatePackage; using NuGet.Versioning; @@ -17,16 +18,14 @@ internal sealed class BuiltInTemplatePackageProvider(BuiltInTemplatePackageProvi public ITemplatePackageProviderFactory Factory { get; } = factory; +#pragma warning disable CS0067 /// /// We don't trigger this event, we could complicate our life with FileSystemWatcher. - /// But since "dotnet new" is short lived process is not worth it, plus it would cause some perf hit... - /// To avoid warnings about being unused, implement empty add/remove accessors. + /// But since "dotnet new" is short lived process is not worth it, plus it would cause + /// some perf hit... /// - public event Action? TemplatePackagesChanged - { - add { } - remove { } - } + public event Action TemplatePackagesChanged; +#pragma warning restore CS0067 public Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken) { @@ -45,12 +44,14 @@ private static IEnumerable GetTemplateFolders(IEngineEnvironmentSettings { var templateFoldersToInstall = new List(); - var sdksDirectory = new DirectoryInfo(MSBuildForwardingAppWithoutLogging.GetMSBuildSDKsPath()); - var sdkDirectory = sdksDirectory.Parent; - var sdkPath = sdkDirectory?.FullName ?? string.Empty; - var dotnetRootPath = sdkDirectory?.Parent?.Parent?.FullName ?? string.Empty; +#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file + var sdkDirectory = Path.GetDirectoryName(typeof(Utils.DotnetFiles).Assembly.Location); +#pragma warning restore IL3000 + + var dotnetRootPath = Path.GetDirectoryName(Path.GetDirectoryName(sdkDirectory)); + // First grab templates from dotnet\templates\M.m folders, in ascending order, up to our version - string templatesRootFolder = Path.Combine(dotnetRootPath, "templates"); + string templatesRootFolder = Path.GetFullPath(Path.Combine(dotnetRootPath, "templates")); if (Directory.Exists(templatesRootFolder)) { IReadOnlyDictionary parsedNames = GetVersionDirectoriesInDirectory(templatesRootFolder); @@ -61,7 +62,7 @@ private static IEnumerable GetTemplateFolders(IEngineEnvironmentSettings } // Now grab templates from our base folder, if present. - string templatesDir = Path.Combine(sdkPath, "Templates"); + string templatesDir = Path.Combine(sdkDirectory, "Templates"); if (Directory.Exists(templatesDir)) { templateFoldersToInstall.Add(templatesDir); @@ -78,7 +79,7 @@ private static IReadOnlyDictionary GetVersionDirectorie foreach (string directory in Directory.EnumerateDirectories(fullPath, "*.*", SearchOption.TopDirectoryOnly)) { - if (SemanticVersion.TryParse(Path.GetFileName(directory), out SemanticVersion? versionInfo) && versionInfo is not null) + if (SemanticVersion.TryParse(Path.GetFileName(directory), out SemanticVersion versionInfo)) { versionFileInfo.Add(directory, versionInfo); } @@ -91,7 +92,7 @@ internal static IList GetBestVersionsByMajorMinor(IReadOnlyDictionary bestVersionsByBucket = new Dictionary(); - Version? sdkVersion = typeof(NewCommandParser).Assembly.GetName().Version; + Version sdkVersion = typeof(NewCommandParser).Assembly.GetName().Version; foreach (KeyValuePair dirInfo in versionDirInfo) { var majorMinorDirVersion = new Version(dirInfo.Value.Major, dirInfo.Value.Minor); diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs index 3ddb261d7340..318f1ec64418 100644 --- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs +++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs @@ -110,6 +110,8 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin projectPath = _projectFullPath; } + Stopwatch watch = new(); + Stopwatch innerBuildWatch = new(); bool IsSdkStyleProject = false; IReadOnlyList? targetFrameworks = null; string? targetFramework = null; @@ -117,6 +119,7 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin try { + watch.Start(); _logger?.LogDebug("Evaluating project: {0}", projectPath); MSBuildProject evaluatedProject = RunEvaluate(projectPath); @@ -161,11 +164,13 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin //For multi-target project, we need to do additional evaluation for each target framework. Dictionary evaluatedTfmBasedProjects = []; + innerBuildWatch.Start(); foreach (string tfm in targetFrameworks) { _logger?.LogDebug("Evaluating project for target framework: {0}", tfm); evaluatedTfmBasedProjects[tfm] = RunEvaluate(projectPath, tfm); } + innerBuildWatch.Stop(); _logger?.LogDebug("Project is SDK style, multi-target, evaluation succeeded."); return result = MultiTargetEvaluationResult.CreateSuccess(projectPath, evaluatedProject, evaluatedTfmBasedProjects); @@ -177,6 +182,9 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin } finally { + watch.Stop(); + innerBuildWatch.Stop(); + string? targetFrameworksString = null; if (targetFrameworks != null) @@ -196,7 +204,13 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin { "TargetFrameworks", targetFrameworksString ?? ""}, }; - TelemetryEventEntry.TrackEvent("new/msbuild-eval", properties); + Dictionary measurements = new() + { + { "EvaluationTime", watch.ElapsedMilliseconds }, + { "InnerEvaluationTime", innerBuildWatch.ElapsedMilliseconds } + }; + + TelemetryEventEntry.TrackEvent("new/msbuild-eval", properties, measurements); } } diff --git a/src/Cli/dotnet/Commands/New/NewCommandParser.cs b/src/Cli/dotnet/Commands/New/NewCommandParser.cs index 68b5268c8579..4a2f614f2ca8 100644 --- a/src/Cli/dotnet/Commands/New/NewCommandParser.cs +++ b/src/Cli/dotnet/Commands/New/NewCommandParser.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using System.Diagnostics; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation; using Microsoft.DotNet.Cli.Commands.New.PostActions; @@ -15,6 +16,7 @@ using Microsoft.TemplateEngine.Abstractions.Constraints; using Microsoft.TemplateEngine.Abstractions.TemplatePackage; using Microsoft.TemplateEngine.Cli; +using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.TemplateEngine.Cli.PostActionProcessors; using Command = System.CommandLine.Command; diff --git a/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs b/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs index 77e48fe7b32a..cf803c32c3c3 100644 --- a/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs +++ b/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.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. +#nullable disable + using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using Microsoft.TemplateEngine.Abstractions; @@ -20,8 +22,8 @@ internal OptionalWorkloadProvider(ITemplatePackageProviderFactory factory, IEngi public ITemplatePackageProviderFactory Factory { get; } - // To avoid warnings about being unused, implement empty add/remove accessors. - event Action? ITemplatePackageProvider.TemplatePackagesChanged + // To avoid warnings about unused, its implemented via add/remove + event Action ITemplatePackageProvider.TemplatePackagesChanged { add { } remove { } @@ -31,13 +33,14 @@ public Task> GetAllTemplatePackagesAsync(Cancell { var list = new List(); var optionalWorkloadLocator = new TemplateLocator.TemplateLocator(); - var sdksDirectory = new DirectoryInfo(MSBuildForwardingAppWithoutLogging.GetMSBuildSDKsPath()); - var sdkDirectory = sdksDirectory?.Parent; - var sdkVersion = sdkDirectory?.Name; - var dotnetRootPath = sdkDirectory?.Parent?.Parent; +#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file + var sdkDirectory = Path.GetDirectoryName(typeof(DotnetFiles).Assembly.Location); +#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file + var sdkVersion = Path.GetFileName(sdkDirectory); + var dotnetRootPath = Path.GetDirectoryName(Path.GetDirectoryName(sdkDirectory)); string userProfileDir = CliFolderPathCalculator.DotnetUserProfileFolderPath; - var packages = optionalWorkloadLocator.GetDotnetSdkTemplatePackages(sdkVersion, dotnetRootPath?.FullName, userProfileDir); + var packages = optionalWorkloadLocator.GetDotnetSdkTemplatePackages(sdkVersion, dotnetRootPath, userProfileDir); var fileSystem = _environmentSettings.Host.FileSystem; foreach (var packageInfo in packages) { diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 9300fa1b8310..ab7d0ba0542b 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -5,6 +5,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Commands.Build; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; @@ -12,6 +13,7 @@ using Microsoft.DotNet.Cli.Utils; using NuGet.Commands; using NuGet.Common; +using NuGet.Packaging; namespace Microsoft.DotNet.Cli.Commands.Pack; @@ -38,7 +40,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui bool noRestore = noBuild || parseResult.HasOption(definition.NoRestoreOption); - return DotNetCommandFactory.CreateVirtualOrPhysicalCommand( + return CommandFactory.CreateVirtualOrPhysicalCommand( definition, definition.SlnOrProjectOrFileArgument, (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand( diff --git a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs b/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs index 370d35d6c816..7c418218f52d 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommandParser.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.CommandLine; using Microsoft.DotNet.Cli.CommandLine; namespace Microsoft.DotNet.Cli.Commands.Pack; diff --git a/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs b/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs index 9ec9c4acd7a1..36d44cee3931 100644 --- a/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs +++ b/src/Cli/dotnet/Commands/Project/ProjectCommandParser.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.CommandLine; using Microsoft.DotNet.Cli.Commands.Project.Convert; using Microsoft.DotNet.Cli.Extensions; diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index 6188060bf16d..bb68394da143 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -44,7 +44,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui bool noRestore = noBuild || parseResult.HasOption(definition.NoRestoreOption); - return DotNetCommandFactory.CreateVirtualOrPhysicalCommand( + return CommandFactory.CreateVirtualOrPhysicalCommand( definition, definition.SlnOrProjectOrFileArgument, (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand( diff --git a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs index 8f2e96fb3f91..2743d1ffe048 100644 --- a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs @@ -24,7 +24,7 @@ public static CommandBase FromParseResult(ParseResult result, string? msbuildPat result.HandleDebugSwitch(); result.ShowHelpOrErrorIfAppropriate(); - return DotNetCommandFactory.CreateVirtualOrPhysicalCommand( + return CommandFactory.CreateVirtualOrPhysicalCommand( definition, definition.SlnOrProjectOrFileArgument, static (msbuildArgs, appFilePath) => diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs index 4a16683d53f8..0d3ab62507b8 100644 --- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs +++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs @@ -48,9 +48,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 @@ -76,7 +80,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"; @@ -87,7 +91,7 @@ public static void TrackRunEvent( } } - TelemetryEventEntry.TrackEvent(RunEventName, properties); + TelemetryEventEntry.TrackEvent(RunEventName, properties, measurements); } /// diff --git a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs index 708e9d0fa93e..61d4b803baa7 100644 --- a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs @@ -38,7 +38,7 @@ public ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolManifestFi : base(result) { _packageToolIdentityArgument = result.GetValue(Definition.PackageIdentityArgument); - _forwardArguments = result.GetValue(Definition.CommandArgument) ?? []; + _forwardArguments = result.GetValue(Definition.CommandArgument) ?? Enumerable.Empty(); _allowRollForward = result.GetValue(Definition.RollForwardOption); _configFile = result.GetValue(Definition.ConfigOption); _sources = result.GetValue(Definition.SourceOption) ?? []; diff --git a/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs b/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs index 72262f1294c0..4b42713ae006 100644 --- a/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs @@ -34,7 +34,7 @@ public override int Execute() public static int ExecuteCommand(LocalToolsCommandResolver commandResolver, string? toolCommandName, IEnumerable? argumentsToForward, bool allowRollForward) { using var _ = Activities.Source.StartActivity("execute-local-tool"); - CommandSpec? commandSpec = commandResolver.ResolveStrict(new CommandResolverArguments() + CommandSpec commandSpec = commandResolver.ResolveStrict(new CommandResolverArguments() { // since LocalToolsCommandResolver is a resolver, and all resolver input have dotnet- CommandName = $"dotnet-{toolCommandName}", @@ -49,4 +49,4 @@ public static int ExecuteCommand(LocalToolsCommandResolver commandResolver, stri var result = CommandFactoryUsingResolver.Create(commandSpec).Execute(); return result.ExitCode; } -} +} \ No newline at end of file diff --git a/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs b/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs index 84249ba7b5c4..f043eafc93be 100644 --- a/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs +++ b/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs @@ -11,20 +11,30 @@ internal class ToolCommandSpecCreator { public static CommandSpec CreateToolCommandSpec(string toolName, string toolExecutable, string toolRunner, bool allowRollForward, IEnumerable commandArguments) { - var environment = ActivityContextFactory.MakeActivityContextEnvironment(); - switch (toolRunner) + if (toolRunner == "dotnet") { - case "dotnet": - if (allowRollForward) - { - commandArguments = ["--allow-roll-forward", .. commandArguments]; - } - return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer(toolExecutable, commandArguments, environment); - case "executable": - var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments); - return new CommandSpec(toolExecutable, escapedArgs, environment); - default: - throw new GracefulException(string.Format(CliStrings.ToolSettingsUnsupportedRunner, toolName, toolRunner)); + if (allowRollForward) + { + commandArguments = ["--allow-roll-forward", .. commandArguments]; + } + + return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer( + toolExecutable, + commandArguments); + } + else if (toolRunner == "executable") + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( + commandArguments); + + return new CommandSpec( + toolExecutable, + escapedArgs); + } + else + { + throw new GracefulException(string.Format(CliStrings.ToolSettingsUnsupportedRunner, + toolName, toolRunner)); } } } diff --git a/src/Cli/dotnet/DotNetCommandFactory.cs b/src/Cli/dotnet/DotNetCommandFactory.cs new file mode 100644 index 000000000000..dcb70b05e6c9 --- /dev/null +++ b/src/Cli/dotnet/DotNetCommandFactory.cs @@ -0,0 +1,47 @@ +// 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 System.CommandLine.Invocation; +using System.Diagnostics; +using Microsoft.DotNet.Cli.CommandFactory; +using Microsoft.DotNet.Cli.Utils; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli; + +public class DotNetCommandFactory(bool alwaysRunOutOfProc = false, string currentWorkingDirectory = null) : ICommandFactory +{ + private readonly bool _alwaysRunOutOfProc = alwaysRunOutOfProc; + private readonly string _currentWorkingDirectory = currentWorkingDirectory; + + public ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration) + { + if (!_alwaysRunOutOfProc && TryGetBuiltInCommand(commandName, out var builtInCommand)) + { + Debug.Assert(framework == null, "BuiltInCommand doesn't support the 'framework' argument."); + Debug.Assert(configuration == Constants.DefaultConfiguration, "BuiltInCommand doesn't support the 'configuration' argument."); + + return new BuiltInCommand(commandName, args, builtInCommand); + } + + return CommandFactoryUsingResolver.CreateDotNet(commandName, args, framework, configuration, _currentWorkingDirectory); + } + + private static bool TryGetBuiltInCommand(string commandName, out Func commandFunc) + { + var command = Parser.GetBuiltInCommand(commandName); + if (command?.Action is AsynchronousCommandLineAction action) + { + commandFunc = (args) => Parser.Invoke([commandName, .. args]); + return true; + } + commandFunc = null; + return false; + } +} diff --git a/src/Cli/dotnet/Extensions/ActivityExtensions.cs b/src/Cli/dotnet/Extensions/ActivityExtensions.cs deleted file mode 100644 index b29d0d86de94..000000000000 --- a/src/Cli/dotnet/Extensions/ActivityExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.CommandLine; -using System.Diagnostics; - -namespace Microsoft.DotNet.Cli.Extensions; - -internal static class ActivityExtensions -{ - public static void SetDisplayName(this Activity? activity, ParseResult parseResult) - { - if (activity is null) - { - return; - } - - var name = parseResult.GetCommandName(); - activity.DisplayName = name; - activity.SetTag("command.name", name); - } -} diff --git a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs index 865ae37cdf18..dc23ee379040 100644 --- a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs +++ b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs @@ -14,27 +14,25 @@ namespace Microsoft.DotNet.Cli.Extensions; public static class ParseResultExtensions { - /// + /// /// Finds the command of the parse result and invokes help for that command. /// If no command is specified, invokes help for the application. - /// - /// + /// + /// /// This is accomplished by finding a set of tokens that should be valid and appending a help token /// to that list, then re-parsing the list of tokens. This is not ideal - either we should have a direct way /// of invoking help for a ParseResult, or we should eliminate this custom, ad-hoc help invocation by moving /// more situations that want to show help into Parsing Errors (which trigger help in the default System.CommandLine pipeline) /// or custom Invocation Middleware, so we can more easily create our version of a HelpResult type. - /// + /// public static void ShowHelp(this ParseResult parseResult) { - // Take from the start of the list until we hit an option/--/unparsed token. - // Since commands can have arguments, we must take those as well in order to get accurate help. - var filteredTokenValues = parseResult.Tokens.TakeWhile(token => - token.Type == TokenType.Argument - || token.Type == TokenType.Command - || token.Type == TokenType.Directive) - .Select(t => t.Value); - Parser.Parse([.. filteredTokenValues, "-h"]).Invoke(); + // take from the start of the list until we hit an option/--/unparsed token + // since commands can have arguments, we must take those as well in order to get accurate help + Parser.Parse([ + ..parseResult.Tokens.TakeWhile(token => token.Type == TokenType.Argument || token.Type == TokenType.Command || token.Type == TokenType.Directive).Select(t => t.Value), + "-h" + ]).Invoke(); } public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult) @@ -47,26 +45,24 @@ public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult) var rawResourcePartsForThisLocale = DistinctFormatStringParts(CliStrings.UnrecognizedCommandOrArgument); return ErrorContainsAllParts(error.Message, rawResourcePartsForThisLocale); }); - - if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors - || parseResult.Errors.Except(unrecognizedTokenErrors).Any()) + if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors || + parseResult.Errors.Except(unrecognizedTokenErrors).Any()) { throw new CommandParsingException( - message: string.Join(Environment.NewLine, parseResult.Errors.Select(e => e.Message)), + message: string.Join(Environment.NewLine, + parseResult.Errors.Select(e => e.Message)), parseResult: parseResult); } } - /// - /// Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking. - /// - static string[] DistinctFormatStringParts(string formatString) => - // Match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'. - Regex.Split(formatString, @"{[0-9]+}"); + ///Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking + static string[] DistinctFormatStringParts(string formatString) + { + return Regex.Split(formatString, @"{[0-9]+}"); // match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}' + } + - /// - /// Given a string and a series of parts, ensures that all parts are present in the string in sequential order. - /// + /// given a string and a series of parts, ensures that all parts are present in the string in sequential order static bool ErrorContainsAllParts(ReadOnlySpan error, string[] parts) { foreach (var part in parts) @@ -77,29 +73,39 @@ static bool ErrorContainsAllParts(ReadOnlySpan error, string[] parts) error = error.Slice(foundIndex + part.Length); continue; } - - return false; + else + { + return false; + } } - return true; } } - public static string RootSubCommandResult(this ParseResult parseResult) => parseResult.RootCommandResult.Children? - .Select(child => parseResult.GetSymbolResultValue(child)) - .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty; + public static string RootSubCommandResult(this ParseResult parseResult) + { + return parseResult.RootCommandResult.Children? + .Select(child => GetSymbolResultValue(parseResult, child)) + .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty; + } - public static bool IsDotnetBuiltInCommand(this ParseResult parseResult) => - string.IsNullOrEmpty(parseResult.RootSubCommandResult()) - || Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null; + public static bool IsDotnetBuiltInCommand(this ParseResult parseResult) + { + return string.IsNullOrEmpty(parseResult.RootSubCommandResult()) || + Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null; + } - public static bool IsTopLevelDotnetCommand(this ParseResult parseResult) => - parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult()); + public static bool IsTopLevelDotnetCommand(this ParseResult parseResult) + { + return parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult()); + } - public static bool CanBeInvoked(this ParseResult parseResult) => - Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null - || parseResult.Tokens.Any(token => token.Type == TokenType.Directive) - || (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand))); + public static bool CanBeInvoked(this ParseResult parseResult) + { + return Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null || + parseResult.Tokens.Any(token => token.Type == TokenType.Directive) || + (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand))); + } public static int HandleMissingCommand(this ParseResult parseResult) { @@ -108,8 +114,12 @@ public static int HandleMissingCommand(this ParseResult parseResult) return 1; } - public static string[] GetArguments(this ParseResult parseResult) => - parseResult.Tokens.Select(t => t.Value).ToArray().GetSubArguments(); + public static string[] GetArguments(this ParseResult parseResult) + { + return parseResult.Tokens.Select(t => t.Value) + .ToArray() + .GetSubArguments(); + } public static string[] GetSubArguments(this string[] args) { @@ -121,30 +131,54 @@ public static string[] GetSubArguments(this string[] args) var runArgs = dashDashIndex > -1 ? subargs.GetRange(dashDashIndex, subargs.Count() - dashDashIndex) : []; subargs = dashDashIndex > -1 ? subargs.GetRange(0, dashDashIndex) : subargs; - // Remove top level command (ex build or publish). - var subargsFiltered = subargs - .SkipWhile(arg => Parser.RootCommand.DiagOption.Name.Equals(arg) - || Parser.RootCommand.DiagOption.Aliases.Contains(arg) - || arg.Equals("dotnet")) - .Skip(1); + return + [ + .. subargs + .SkipWhile(arg => Parser.RootCommand.DiagOption.Name.Equals(arg) || Parser.RootCommand.DiagOption.Aliases.Contains(arg) || arg.Equals("dotnet")) + .Skip(1), // remove top level command (ex build or publish) + .. runArgs + ]; + } + + public static bool DiagOptionPrecedesSubcommand(this string[] args, string subCommand) + { + if (string.IsNullOrEmpty(subCommand)) + { + return true; + } + + for (var i = 0; i < args.Length; i++) + { + if (args[i].Equals(subCommand)) + { + return false; + } + else if (Parser.RootCommand.DiagOption.Name.Equals(args) || Parser.RootCommand.DiagOption.Aliases.Contains(args[i])) + { + return true; + } + } - return [.. subargsFiltered, .. runArgs]; + return false; } - private static string? GetSymbolResultValue(this ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch + private static string? GetSymbolResultValue(ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch { CommandResult commandResult => commandResult.Command.Name, ArgumentResult argResult => argResult.Tokens.FirstOrDefault()?.Value, _ => parseResult.GetResult(Parser.RootCommand.DotnetSubCommand)?.GetValueOrDefault() }; - public static IEnumerable? GetRunCommandShorthandProjectValues(this ParseResult parseResult) => - parseResult.GetRunPropertyOptions(true)?.Where(property => !property.Contains("=")); + public static IEnumerable? GetRunCommandShorthandProjectValues(this ParseResult parseResult) + { + var properties = GetRunPropertyOptions(parseResult, true); + return properties?.Where(property => !property.Contains("=")); + } public static IEnumerable GetRunCommandPropertyValues(this ParseResult parseResult) { - var shorthandProperties = parseResult.GetRunPropertyOptions(true)?.Where(property => property.Contains("=")); - var longhandProperties = parseResult.GetRunPropertyOptions(false); + var shorthandProperties = GetRunPropertyOptions(parseResult, true)?.Where(property => property.Contains("=")); + var longhandProperties = GetRunPropertyOptions(parseResult, false); return (shorthandProperties, longhandProperties) switch { (null, null) => Enumerable.Empty(), @@ -154,7 +188,7 @@ public static IEnumerable GetRunCommandPropertyValues(this ParseResult p }; } - private static IEnumerable? GetRunPropertyOptions(this ParseResult parseResult, bool shorthand) + private static IEnumerable? GetRunPropertyOptions(ParseResult parseResult, bool shorthand) { var optionString = shorthand ? "-p" : "--property"; var propertyOptions = parseResult.CommandResult.Children.Where(c => GetOptionTokenOrDefault(c)?.Value.Equals(optionString) ?? false); @@ -180,26 +214,4 @@ public static void HandleDebugSwitch(this ParseResult parseResult) DebugHelper.WaitForDebugger(); } } - - public static string GetCommandName(this ParseResult parseResult) - { - // Walk the parent command tree to find the top-level command name and get the full command name for this ParseResult. - List parentNames = [parseResult.CommandResult.Command.Name]; - var current = parseResult.CommandResult.Parent; - while (current is CommandResult parentCommandResult) - { - parentNames.Add(parentCommandResult.Command.Name); - current = parentCommandResult.Parent; - } - parentNames.Reverse(); - - // Options that perform terminating actions are considered part of the command name as they are essentially subcommands themselves. - // Example: dotnet --version - if (parseResult.Action is InvocableOptionAction { Terminating: true } optionAction) - { - parentNames.Add(optionAction.Option.Name); - } - - return string.Join(' ', parentNames); - } } diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs index 42b129db7067..475d59655a3b 100644 --- a/src/Cli/dotnet/Parser.cs +++ b/src/Cli/dotnet/Parser.cs @@ -1,8 +1,10 @@ // 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 System.CommandLine; -using System.CommandLine.Help; +using System.CommandLine.Invocation; using System.CommandLine.StaticCompletions; using System.Reflection; using Microsoft.DotNet.Cli.Commands; @@ -45,6 +47,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.TemplateEngine.Cli; +using Microsoft.TemplateEngine.Cli.Help; using Command = System.CommandLine.Command; namespace Microsoft.DotNet.Cli; @@ -63,14 +66,19 @@ private static DotNetCommandDefinition CreateCommand() { rootCommand.Options.RemoveAt(i); } - else if (option is HelpOption helpOption) + else if (option is System.CommandLine.Help.HelpOption helpOption) { - helpOption.Action = new PrintHelpAction(helpOption, DotnetHelpBuilder.Instance.Value); + helpOption.Action = new DotnetHelpAction() + { + Builder = DotnetHelpBuilder.Instance.Value + }; + option.Description = CliStrings.ShowHelpDescription; } } // Augment the definition of each subcommand with command-specific actions and completions. + AddCommandParser.ConfigureCommand(rootCommand.AddCommand); BuildCommandParser.ConfigureCommand(rootCommand.BuildCommand); BuildServerCommandParser.ConfigureCommand(rootCommand.BuildServerCommand); @@ -114,10 +122,7 @@ private static DotNetCommandDefinition CreateCommand() WorkloadCommandParser.ConfigureCommand(rootCommand.WorkloadCommand); CompletionsCommandParser.ConfigureCommand(rootCommand.CompletionsCommand); - rootCommand.DiagOption.Action = new HandleDiagnosticAction(rootCommand.DiagOption); - rootCommand.VersionOption.Action = new PrintVersionAction(rootCommand.VersionOption); - rootCommand.InfoOption.Action = new PrintInfoAction(rootCommand.InfoOption); - rootCommand.CliSchemaOption.Action = new PrintCliSchemaAction(rootCommand.CliSchemaOption); + rootCommand.CliSchemaOption.Action = new PrintCliSchemaAction(); // TODO: https://github.com/dotnet/sdk/issues/52661 // https://github.com/NuGet/NuGet.Client/blob/bf048eb714eb6b1912ba868edca4c7cfec454841/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs @@ -128,13 +133,13 @@ private static DotNetCommandDefinition CreateCommand() { if (parseResult.GetValue(rootCommand.DiagOption) && parseResult.Tokens.Count == 1) { - // When user does not specify any args except of diagnostics ("dotnet -d"), - // we do nothing as HandleDiagnosticAction already enabled the diagnostic output. + // when user does not specify any args except of diagnostics ("dotnet -d"), we do nothing + // as Program.ProcessArgs already enabled the diagnostic output return 0; } else { - // When user does not specify any args (just "dotnet"), a usage needs to be printed. + // when user does not specify any args (just "dotnet"), a usage needs to be printed parseResult.InvocationConfiguration.Output.WriteLine(CliUsage.HelpText); return 0; } @@ -143,14 +148,14 @@ private static DotNetCommandDefinition CreateCommand() return rootCommand; } - public static Command? GetBuiltInCommand(string commandName) => + public static Command GetBuiltInCommand(string commandName) => RootCommand.Subcommands.FirstOrDefault(c => c.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); /// /// Implements token-per-line response file handling for the CLI. We use this instead of the built-in S.CL handling /// to ensure backwards-compatibility with MSBuild. /// - public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList? replacementTokens, out string? errorMessage) + public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList replacementTokens, out string errorMessage) { var filePath = Path.GetFullPath(tokenToReplace); if (File.Exists(filePath)) @@ -210,7 +215,7 @@ public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList public static int Invoke(string[] args) => Invoke(Parse(args)); public static Task InvokeAsync(string[] args, CancellationToken cancellationToken = default) => InvokeAsync(Parse(args), cancellationToken); - internal static int ExceptionHandler(Exception? exception, ParseResult parseResult) + internal static int ExceptionHandler(Exception exception, ParseResult parseResult) { if (exception is TargetInvocationException) { @@ -230,13 +235,13 @@ internal static int ExceptionHandler(Exception? exception, ParseResult parseResu exception.Message.Red().Bold()); parseResult.ShowHelp(); } - else if (exception is not null && exception.GetType().Name.Equals("WorkloadManifestCompositionException")) + else if (exception.GetType().Name.Equals("WorkloadManifestCompositionException")) { Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ? exception.ToString().Red().Bold() : exception.Message.Red().Bold()); } - else if (exception is not null) + else { Reporter.Error.Write("Unhandled exception: ".Red().Bold()); Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ? @@ -324,7 +329,7 @@ public override void Write(HelpContext context) } else if (command is FormatCommandDefinition format) { - var arguments = context.ParseResult.GetValue(format.Arguments) ?? []; + var arguments = context.ParseResult.GetValue(format.Arguments); new FormatForwardingApp([.. arguments, .. helpArgs]).Execute(); } else if (command is FsiCommandDefinition) @@ -346,16 +351,14 @@ public override void Write(HelpContext context) if (command.Name.Equals(ListReferenceCommandDefinition.Name)) { - Command? listCommand = command.Parents.Single() as Command; - if (listCommand is not null) + Command listCommand = command.Parents.Single() as Command; + + for (int i = 0; i < listCommand.Arguments.Count; i++) { - for (int i = 0; i < listCommand.Arguments.Count; i++) + if (listCommand.Arguments[i].Name == CliStrings.SolutionOrProjectArgumentName) { - if (listCommand.Arguments[i].Name == CliStrings.SolutionOrProjectArgumentName) - { - // Name is immutable now, so we create a new Argument with the right name.. - listCommand.Arguments[i] = ListCommandDefinition.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription); - } + // Name is immutable now, so we create a new Argument with the right name.. + listCommand.Arguments[i] = Commands.Hidden.List.ListCommandDefinition.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription); } } } @@ -377,4 +380,15 @@ public override void Write(HelpContext context) } } } + + private class PrintCliSchemaAction : SynchronousCommandLineAction + { + public override bool Terminating => true; + + public override int Invoke(ParseResult parseResult) + { + CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.InvocationConfiguration.Output, Program.TelemetryClient); + return 0; + } + } } diff --git a/src/Cli/dotnet/ParserOptionActions.cs b/src/Cli/dotnet/ParserOptionActions.cs deleted file mode 100644 index 8186712fa4de..000000000000 --- a/src/Cli/dotnet/ParserOptionActions.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.CommandLine; -using System.CommandLine.Invocation; -using Microsoft.DotNet.Cli.Commands.Workload; -using Microsoft.DotNet.Cli.Extensions; -using Microsoft.DotNet.Cli.Help; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Configurer; -using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; - -namespace Microsoft.DotNet.Cli; - -/// -/// Represents an option that contains an invocable action. -/// These are essentially commands that are only defined as an option. -/// -internal abstract class InvocableOptionAction(Option option) : SynchronousCommandLineAction -{ - /// - /// The option for which this action is bound. - /// - public Option Option { get; } = option; -} - -internal class HandleDiagnosticAction(Option option) : InvocableOptionAction(option) -{ - public override bool Terminating => false; - - public override int Invoke(ParseResult parseResult) - { - // Only set verbose output on built-in commands. - if (!parseResult.IsDotnetBuiltInCommand()) - { - return 0; - } - - // Determine whether the diagnostic option should be attached to the dotnet command or the subcommand. - if (DiagOptionPrecedesSubcommand(parseResult.Tokens.Select(t => t.Value), parseResult.RootSubCommandResult())) - { - Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString); - CommandLoggingContext.SetVerbose(true); - Reporter.Reset(); - - var home = Env.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName); - if (!string.IsNullOrEmpty(home)) - { - // Output DOTNET_CLI_HOME usage when verbosity is enabled. - Reporter.Verbose.WriteLine(string.Format(LocalizableStrings.DotnetCliHomeUsed, home, CliFolderPathCalculator.DotnetHomeVariableName)); - } - } - - return 0; - } - - private static bool DiagOptionPrecedesSubcommand(IEnumerable tokens, string subCommand) - { - if (string.IsNullOrEmpty(subCommand)) - { - return true; - } - - foreach (var token in tokens) - { - if (token == subCommand) - { - return false; - } - - if (Parser.RootCommand.DiagOption.Name == token - || Parser.RootCommand.DiagOption.Aliases.Contains(token)) - { - return true; - } - } - - return false; - } -} - -internal class PrintHelpAction(Option option, HelpBuilder builder) : InvocableOptionAction(option) -{ - public override bool Terminating => true; - public override bool ClearsParseErrors => true; - - private HelpBuilder Builder { get; } = builder; - - public override int Invoke(ParseResult parseResult) - { - var command = parseResult.CommandResult.Command; - var output = parseResult.InvocationConfiguration.Output; - var helpContext = new HelpContext(Builder, command, output, parseResult); - Builder.Write(helpContext); - - return 0; - } -} - -internal class PrintVersionAction(Option option) : InvocableOptionAction(option) -{ - public override bool Terminating => true; - - public override int Invoke(ParseResult parseResult) - { - // Only print for top-level commands. - if (!parseResult.IsTopLevelDotnetCommand()) - { - return 0; - } - - Reporter.Output.WriteLine(Product.Version); - - return 0; - } -} - -internal class PrintInfoAction(Option option) : InvocableOptionAction(option) -{ - public override bool Terminating => true; - - public override int Invoke(ParseResult parseResult) - { - // Only print for top-level commands. - if (!parseResult.IsTopLevelDotnetCommand()) - { - return 0; - } - - DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; - var commitSha = versionFile.CommitSha ?? "N/A"; - Reporter.Output.WriteLine($"{LocalizableStrings.DotNetSdkInfoLabel}"); - Reporter.Output.WriteLine($" Version: {Product.Version}"); - Reporter.Output.WriteLine($" Commit: {commitSha}"); - Reporter.Output.WriteLine($" Workload version: {WorkloadInfoHelper.GetWorkloadsVersion()}"); - Reporter.Output.WriteLine($" MSBuild version: {MSBuildForwardingAppWithoutLogging.MSBuildVersion}"); - Reporter.Output.WriteLine(); - Reporter.Output.WriteLine($"{LocalizableStrings.DotNetRuntimeInfoLabel}"); - Reporter.Output.WriteLine($" OS Name: {RuntimeEnvironment.OperatingSystem}"); - Reporter.Output.WriteLine($" OS Version: {RuntimeEnvironment.OperatingSystemVersion}"); - Reporter.Output.WriteLine($" OS Platform: {RuntimeEnvironment.OperatingSystemPlatform}"); - Reporter.Output.WriteLine($" RID: {GetDisplayRid(versionFile)}"); - Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}"); - Reporter.Output.WriteLine(); - Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}"); - new WorkloadInfoHelper(isInteractive: false).ShowWorkloadsInfo(showVersion: false); - - return 0; - } - - private static string? GetDisplayRid(DotnetVersionFile versionFile) - { - FrameworkDependencyFile fxDepsFile = new(); - string currentRid = RuntimeInformation.RuntimeIdentifier; - // If the current RID isn't supported by the shared framework, display the RID the CLI was built with instead, - // so the user knows which RID they should put in their "runtimes" section. - return fxDepsFile.IsRuntimeSupported(currentRid) ? currentRid : versionFile.BuildRid; - } -} - -internal class PrintCliSchemaAction(Option option) : InvocableOptionAction(option) -{ - public override bool Terminating => true; - - public override int Invoke(ParseResult parseResult) - { - CliSchema.PrintCliSchema(parseResult, parseResult.InvocationConfiguration.Output, Program.TelemetryInstance); - - return 0; - } -} diff --git a/src/Cli/dotnet/PerformanceLogEventListener.cs b/src/Cli/dotnet/PerformanceLogEventListener.cs new file mode 100644 index 000000000000..201006e5c011 --- /dev/null +++ b/src/Cli/dotnet/PerformanceLogEventListener.cs @@ -0,0 +1,160 @@ +// 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 System.Diagnostics.Tracing; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.Cli; + +internal sealed class PerformanceLogEventListener : EventListener +{ + internal struct ProviderConfiguration + { + internal string Name { get; set; } + internal EventKeywords Keywords { get; set; } + internal EventLevel Level { get; set; } + } + + private static readonly ProviderConfiguration[] s_config = + [ + new ProviderConfiguration() + { + Name = "Microsoft-Dotnet-CLI-Performance", + Keywords = EventKeywords.All, + Level = EventLevel.Verbose + } + ]; + + private const char EventDelimiter = '\n'; + private StreamWriter _writer; + + [ThreadStatic] + private static StringBuilder s_builder = new(); + + internal static PerformanceLogEventListener Create(IFileSystem fileSystem, string logDirectory) + { + // Only create a listener if the log directory exists. + if (string.IsNullOrWhiteSpace(logDirectory) || !fileSystem.Directory.Exists(logDirectory)) + { + return null; + } + + PerformanceLogEventListener eventListener = null; + try + { + // Initialization happens as a separate step and not in the constructor to ensure that + // if an exception is thrown during init, we have the opportunity to dispose of the listener, + // which will disable any EventSources that have been enabled. Any EventSources that existed before + // this EventListener will be passed to OnEventSourceCreated before our constructor is called, so + // we if we do this work in the constructor, and don't get an opportunity to call Dispose, the + // EventSources will remain enabled even if there aren't any consuming EventListeners. + eventListener = new PerformanceLogEventListener(); + eventListener.Initialize(fileSystem, logDirectory); + } + catch + { + if (eventListener != null) + { + eventListener.Dispose(); + } + } + + return eventListener; + } + + private PerformanceLogEventListener() + { + } + + internal void Initialize(IFileSystem fileSystem, string logDirectory) + { + // Use a GUID disambiguator to make sure that we have a unique file name. + string logFilePath = Path.Combine(logDirectory, $"perf-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")}.log"); + + Stream outputStream = fileSystem.File.OpenFile( + logFilePath, + FileMode.Create, // Create or overwrite. + FileAccess.Write, // Open for writing. + FileShare.Read, // Allow others to read. + 4096, // Default buffer size. + FileOptions.None); // No hints about how the file will be written. + + _writer = new StreamWriter(outputStream); + } + + public override void Dispose() + { + lock (this) + { + if (_writer != null) + { + _writer.Dispose(); + _writer = null; + } + } + + base.Dispose(); + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + try + { + // Enable the provider if it matches a requested configuration. + foreach (ProviderConfiguration entry in s_config) + { + if (entry.Name.Equals(eventSource.Name)) + { + EnableEvents(eventSource, entry.Level, entry.Keywords); + } + } + } + catch + { + // If we fail to enable, just skip it and continue. + } + + base.OnEventSourceCreated(eventSource); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + try + { + if (s_builder == null) + { + s_builder = new StringBuilder(); + } + else + { + s_builder.Clear(); + } + + s_builder.Append($"[{DateTime.UtcNow.ToString("o")}] Event={eventData.EventSource.Name}/{eventData.EventName} ProcessID={Environment.ProcessId} ThreadID={Thread.CurrentThread.ManagedThreadId}\t "); + for (int i = 0; i < eventData.PayloadNames.Count; i++) + { + s_builder.Append($"{eventData.PayloadNames[i]}=\"{eventData.Payload[i]}\" "); + } + + lock (this) + { + if (_writer != null) + { + foreach (ReadOnlyMemory mem in s_builder.GetChunks()) + { + _writer.Write(mem); + } + _writer.Write(EventDelimiter); + } + } + } + catch + { + // If we fail to log an event, just skip it and continue. + } + + base.OnEventWritten(eventData); + } +} diff --git a/src/Cli/dotnet/PerformanceLogEventSource.cs b/src/Cli/dotnet/PerformanceLogEventSource.cs new file mode 100644 index 000000000000..0d1912db6c72 --- /dev/null +++ b/src/Cli/dotnet/PerformanceLogEventSource.cs @@ -0,0 +1,455 @@ +// 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 System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Reflection; +using Microsoft.DotNet.Cli.Utils; +using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; + +namespace Microsoft.DotNet.Cli; + +[EventSource(Name = "Microsoft-Dotnet-CLI-Performance", Guid = "cbd57d06-3b9f-5374-ed53-cfbcc23cf44f")] +internal sealed partial class PerformanceLogEventSource : EventSource +{ + internal static PerformanceLogEventSource Log = new(); + + [NonEvent] + internal void LogStartUpInformation(PerformanceLogStartupInformation startupInfo) + { + if (!IsEnabled()) + { + return; + } + + DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; + string commitSha = versionFile.CommitSha ?? "N/A"; + + LogMachineConfiguration(); + OSInfo(RuntimeEnvironment.OperatingSystem, RuntimeEnvironment.OperatingSystemVersion, RuntimeEnvironment.OperatingSystemPlatform.ToString()); + SDKInfo(Product.Version, commitSha, RuntimeInformation.RuntimeIdentifier, versionFile.BuildRid, AppContext.BaseDirectory); + EnvironmentInfo(Environment.CommandLine); + LogMemoryConfiguration(); + LogDrives(); + + // It's possible that IsEnabled returns true if an out-of-process collector such as ETW is enabled. + // If the perf log hasn't been enabled, then startupInfo will be null, so protect against nullref here. + if (startupInfo != null) + { + if (startupInfo.TimedAssembly != null) + { + AssemblyLoad(startupInfo.TimedAssembly.GetName().Name, startupInfo.AssemblyLoadTime.TotalMilliseconds); + } + + Process currentProcess = Process.GetCurrentProcess(); + TimeSpan latency = startupInfo.MainTimeStamp - currentProcess.StartTime; + HostLatency(latency.TotalMilliseconds); + } + } + + [Event(1)] + internal void OSInfo(string osname, string osversion, string osplatform) + { + WriteEvent(1, osname, osversion, osplatform); + } + + [Event(2)] + internal void SDKInfo(string version, string commit, string currentRid, string buildRid, string basePath) + { + WriteEvent(2, version, commit, currentRid, buildRid, basePath); + } + + [Event(3)] + internal void EnvironmentInfo(string commandLine) + { + WriteEvent(3, commandLine); + } + + [Event(4)] + internal void HostLatency(double timeInMs) + { + WriteEvent(4, timeInMs); + } + + [Event(5)] + internal void CLIStart() + { + WriteEvent(5); + } + + [Event(6)] + internal void CLIStop() + { + WriteEvent(6); + } + + [Event(7)] + internal void FirstTimeConfigurationStart() + { + WriteEvent(7); + } + + [Event(8)] + internal void FirstTimeConfigurationStop() + { + WriteEvent(8); + } + + [Event(9)] + internal void TelemetryRegistrationStart() + { + WriteEvent(9); + } + + [Event(10)] + internal void TelemetryRegistrationStop() + { + WriteEvent(10); + } + + [Event(11)] + internal void TelemetrySaveIfEnabledStart() + { + WriteEvent(11); + } + + [Event(12)] + internal void TelemetrySaveIfEnabledStop() + { + WriteEvent(12); + } + + [Event(13)] + internal void BuiltInCommandStart() + { + WriteEvent(13); + } + + [Event(14)] + internal void BuiltInCommandStop() + { + WriteEvent(14); + } + + [Event(15)] + internal void BuiltInCommandParserStart() + { + WriteEvent(15); + } + + [Event(16)] + internal void BuiltInCommandParserStop() + { + WriteEvent(16); + } + + [Event(17)] + internal void ExtensibleCommandResolverStart() + { + WriteEvent(17); + } + + [Event(18)] + internal void ExtensibleCommandResolverStop() + { + WriteEvent(18); + } + + [Event(19)] + internal void ExtensibleCommandStart() + { + WriteEvent(19); + } + + [Event(20)] + internal void ExtensibleCommandStop() + { + WriteEvent(20); + } + + [Event(21)] + internal void TelemetryClientFlushStart() + { + WriteEvent(21); + } + + [Event(22)] + internal void TelemetryClientFlushStop() + { + WriteEvent(22); + } + + [NonEvent] + internal void LogMachineConfiguration() + { + if (IsEnabled()) + { + MachineConfiguration(Environment.MachineName, Environment.ProcessorCount); + } + } + + [Event(23)] + internal void MachineConfiguration(string machineName, int processorCount) + { + WriteEvent(23, machineName, processorCount); + } + + [NonEvent] + internal void LogDrives() + { + if (IsEnabled()) + { + foreach (DriveInfo driveInfo in DriveInfo.GetDrives()) + { + try + { + DriveConfiguration(driveInfo.Name, driveInfo.DriveFormat, driveInfo.DriveType.ToString(), + (double)driveInfo.TotalSize / 1024 / 1024, (double)driveInfo.AvailableFreeSpace / 1024 / 1024); + } + catch + { + // If we fail to log a drive, skip it and continue. + } + } + } + } + + [Event(24)] + internal void DriveConfiguration(string name, string format, string type, double totalSizeMB, double availableFreeSpaceMB) + { + WriteEvent(24, name, format, type, totalSizeMB, availableFreeSpaceMB); + } + + [Event(25)] + internal void AssemblyLoad(string assemblyName, double timeInMs) + { + WriteEvent(25, assemblyName, timeInMs); + } + + [NonEvent] + internal void LogMemoryConfiguration() + { + if (IsEnabled()) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Interop.MEMORYSTATUSEX memoryStatusEx = new(); + memoryStatusEx.dwLength = (uint)Marshal.SizeOf(memoryStatusEx); + + if (Interop.GlobalMemoryStatusEx(ref memoryStatusEx)) + { + MemoryConfiguration((int)memoryStatusEx.dwMemoryLoad, (int)(memoryStatusEx.ullAvailPhys / 1024 / 1024), + (int)(memoryStatusEx.ullTotalPhys / 1024 / 1024)); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + ProcMemInfo memInfo = new(); + if (memInfo.Valid) + { + MemoryConfiguration(memInfo.MemoryLoad, memInfo.AvailableMemoryMB, memInfo.TotalMemoryMB); + } + } + } + } + + [Event(26)] + internal void MemoryConfiguration(int memoryLoad, int availablePhysicalMB, int totalPhysicalMB) + { + WriteEvent(26, memoryLoad, availablePhysicalMB, totalPhysicalMB); + } + + [NonEvent] + internal void LogMSBuildStart(string fileName, string arguments) + { + if (IsEnabled()) + { + MSBuildStart($"{fileName} {arguments}"); + } + } + + [Event(27)] + internal void MSBuildStart(string cmdline) + { + WriteEvent(27, cmdline); + } + + [Event(28)] + internal void MSBuildStop(int exitCode) + { + WriteEvent(28, exitCode); + } + + [Event(29)] + internal void CreateBuildCommandStart() + { + WriteEvent(29); + } + + [Event(30)] + internal void CreateBuildCommandStop() + { + WriteEvent(30); + } +} + +internal class PerformanceLogStartupInformation +{ + public PerformanceLogStartupInformation(DateTime mainTimeStamp) + { + // Save the main timestamp. + MainTimeStamp = mainTimeStamp; + + // Attempt to load an assembly. + // Ideally, we've picked one that we'll already need, so we're not adding additional overhead. + MeasureModuleLoad(); + } + + internal DateTime MainTimeStamp { get; private set; } + internal Assembly TimedAssembly { get; private set; } + internal TimeSpan AssemblyLoadTime { get; private set; } + + private void MeasureModuleLoad() + { + // Make sure the assembly hasn't been loaded yet. + string assemblyName = "Microsoft.DotNet.Configurer"; + try + { + foreach (Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (loadedAssembly.GetName().Name.Equals(assemblyName)) + { + // If the assembly is already loaded, then bail. + return; + } + } + } + catch + { + // If we fail to enumerate, just bail. + return; + } + + Stopwatch stopWatch = Stopwatch.StartNew(); + Assembly assembly; + try + { + assembly = Assembly.Load(assemblyName); + } + catch + { + return; + } + stopWatch.Stop(); + if (assembly != null) + { + // Save the results. + TimedAssembly = assembly; + AssemblyLoadTime = stopWatch.Elapsed; + } + } +} + +/// +/// Global memory statistics on Windows. +/// +internal static class Interop +{ + [StructLayout(LayoutKind.Sequential)] + internal struct MEMORYSTATUSEX + { + // The length field must be set to the size of this data structure. + internal uint dwLength; + internal uint dwMemoryLoad; + internal ulong ullTotalPhys; + internal ulong ullAvailPhys; + internal ulong ullTotalPageFile; + internal ulong ullAvailPageFile; + internal ulong ullTotalVirtual; + internal ulong ullAvailVirtual; + internal ulong ullAvailExtendedVirtual; + } + + [DllImport("kernel32.dll")] + internal static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); +} + +/// +/// Global memory statistics on Linux. +/// +internal sealed class ProcMemInfo +{ + private const string MemTotal = "MemTotal:"; + private const string MemAvailable = "MemAvailable:"; + + private short _matchingLineCount = 0; + + internal ProcMemInfo() + { + Initialize(); + } + + /// + /// The data in this class is valid if we parsed the file, found, and properly parsed the two matching lines. + /// + internal bool Valid + { + get { return _matchingLineCount == 2; } + } + + internal int MemoryLoad + { + get { return (int)((double)(TotalMemoryMB - AvailableMemoryMB) / TotalMemoryMB * 100); } + } + + internal int AvailableMemoryMB + { + get; + private set; + } + + internal int TotalMemoryMB + { + get; + private set; + } + + private void Initialize() + { + try + { + using (StreamReader reader = new(File.OpenRead("/proc/meminfo"))) + { + string line; + while (!Valid && ((line = reader.ReadLine()) != null)) + { + if (line.StartsWith(MemTotal) || line.StartsWith(MemAvailable)) + { + string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 3) + { + if (MemTotal.Equals(tokens[0])) + { + TotalMemoryMB = (int)Convert.ToUInt64(tokens[1]) / 1024; + _matchingLineCount++; + } + else if (MemAvailable.Equals(tokens[0])) + { + AvailableMemoryMB = (int)Convert.ToUInt64(tokens[1]) / 1024; + _matchingLineCount++; + } + } + } + } + } + } + catch (Exception ex) when (ex is IOException || ex.InnerException is IOException) + { + // in some environments (restricted docker container, shared hosting etc.), + // procfs is not accessible and we get UnauthorizedAccessException while the + // inner exception is set to IOException. Ignore and continue when that happens. + } + } +} diff --git a/src/Cli/dotnet/PerformanceLogManager.cs b/src/Cli/dotnet/PerformanceLogManager.cs new file mode 100644 index 000000000000..3864adceb84c --- /dev/null +++ b/src/Cli/dotnet/PerformanceLogManager.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.Cli; + +internal sealed class PerformanceLogManager +{ + internal const string PerfLogDirEnvVar = "DOTNET_PERFLOG_DIR"; + private const string PerfLogRoot = "PerformanceLogs"; + private const int DefaultNumLogsToKeep = 10; + + private readonly IFileSystem _fileSystem; + private string _perfLogRoot; + + internal static PerformanceLogManager Instance + { + get; + private set; + } + + internal static void InitializeAndStartCleanup(IFileSystem fileSystem) + { + if (Instance == null) + { + Instance = new PerformanceLogManager(fileSystem); + + // Check to see if this instance is part of an already running chain of processes. + string perfLogDir = Env.GetEnvironmentVariable(PerfLogDirEnvVar); + if (!string.IsNullOrEmpty(perfLogDir)) + { + // This process has been provided with a log directory, so use it. + Instance.UseExistingLogDirectory(perfLogDir); + } + else + { + // This process was not provided with a log root, so make a new one. + Instance._perfLogRoot = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, PerfLogRoot); + Instance.CreateLogDirectory(); + + Task.Factory.StartNew(() => + { + Instance.CleanupOldLogs(); + }); + } + } + } + + internal PerformanceLogManager(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + internal string CurrentLogDirectory { get; private set; } + + private void CreateLogDirectory() + { + // Ensure the log root directory exists. + if (!_fileSystem.Directory.Exists(_perfLogRoot)) + { + _fileSystem.Directory.CreateDirectory(_perfLogRoot); + } + + // Create a new perf log directory. + CurrentLogDirectory = Path.Combine(_perfLogRoot, Guid.NewGuid().ToString("N")); + _fileSystem.Directory.CreateDirectory(CurrentLogDirectory); + } + + private void UseExistingLogDirectory(string logDirectory) + { + CurrentLogDirectory = logDirectory; + } + + private void CleanupOldLogs() + { + if (_fileSystem.Directory.Exists(_perfLogRoot)) + { + List logDirectories = []; + foreach (string directoryPath in _fileSystem.Directory.EnumerateDirectories(_perfLogRoot)) + { + logDirectories.Add(new DirectoryInfo(directoryPath)); + } + + // Sort the list. + logDirectories.Sort(new LogDirectoryComparer()); + + // Figure out how many logs to keep. + int numLogsToKeep; + string strNumLogsToKeep = Env.GetEnvironmentVariable("DOTNET_PERF_LOG_COUNT"); + if (!int.TryParse(strNumLogsToKeep, out numLogsToKeep)) + { + numLogsToKeep = DefaultNumLogsToKeep; + + // -1 == keep all logs + if (numLogsToKeep == -1) + { + numLogsToKeep = int.MaxValue; + } + } + + // Skip the first numLogsToKeep elements. + if (logDirectories.Count > numLogsToKeep) + { + // Prune the old logs. + for (int i = logDirectories.Count - numLogsToKeep - 1; i >= 0; i--) + { + try + { + logDirectories[i].Delete(true); + } + catch + { + // Do nothing if a log can't be deleted. + // We'll get another chance next time around. + } + } + } + } + } +} + +/// +/// Used to sort log directories when deciding which ones to delete. +/// +internal sealed class LogDirectoryComparer : IComparer +{ + int IComparer.Compare(DirectoryInfo x, DirectoryInfo y) + { + return x.CreationTime.CompareTo(y.CreationTime); + } +} diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index f0558ad174f8..73a7d7eb6990 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -1,12 +1,17 @@ // 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 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.CommandLine; using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess; +using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Workload; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; @@ -24,329 +29,422 @@ namespace Microsoft.DotNet.Cli; public class Program { - private static readonly string s_toolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; - - private static readonly Activity? s_mainActivity; - private static readonly PosixSignalRegistration s_sigIntRegistration; - private static readonly PosixSignalRegistration s_sigQuitRegistration; - private static readonly PosixSignalRegistration s_sigTermRegistration; - private static readonly string? s_globalJsonState; - - public static ITelemetryClient TelemetryInstance { get; private set; } + private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; - static Program() + public static ITelemetry TelemetryClient; + public static int Main(string[] args) { - var mainTimeStamp = DateTime.Now; - s_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, Shutdown); - s_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, Shutdown); - s_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, Shutdown); - - // Note: This TelemetryClient instance needs to be created prior to calculating ActivityKind and ParentActivityContext, - // used in the main activity creation below. - TelemetryInstance = new TelemetryClient(); - TelemetryEventEntry.Subscribe(TelemetryInstance.TrackEvent); - TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); - - s_mainActivity = Activities.Source.CreateActivity("main", TelemetryClient.ActivityKind, TelemetryClient.ParentActivityContext) - ?.Start() - ?.SetStartTime(Process.GetCurrentProcess().StartTime) - ?.AddTag("process.pid", Process.GetCurrentProcess().Id) - ?.AddTag("process.executable.name", "dotnet"); + // 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)); - if (CommandLoggingContext.IsVerbose) - { - Console.WriteLine($"Telemetry is: {(TelemetryInstance.Enabled ? "Enabled" : "Disabled")}"); - } + using AutomaticEncodingRestorer _ = new(); - // Creates a host-startup activity which includes the global.json state. - using (var hostStartupActivity = Activities.Source.StartActivity("host-startup")) + if (Env.GetEnvironmentVariable("DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING") != "1") { - hostStartupActivity?.SetStartTime(Process.GetCurrentProcess().StartTime); - if (TelemetryInstance.Enabled && hostStartupActivity is not null) + // Setting output encoding is not available on those platforms + if (UILanguageOverride.OperatingSystemSupportsUtf8()) { - // Get the global.json state to report in telemetry along with this command invocation. - s_globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory); - hostStartupActivity?.AddTag("dotnet.globalJson", s_globalJsonState); + Console.OutputEncoding = Encoding.UTF8; } - hostStartupActivity?.SetEndTime(mainTimeStamp)?.SetStatus(ActivityStatusCode.Ok); } - // We have some behaviors in MSBuild that we want to enforce (either when using MSBuild API or by shelling out to it), - // so we set those ASAP as globally as possible. + DebugHelper.HandleDebugSwitch(ref args); + + // Capture the current timestamp to calculate the host overhead. + DateTime mainTimeStamp = DateTime.Now; + TimeSpan startupTime = mainTimeStamp - Process.GetCurrentProcess().StartTime; + + bool perfLogEnabled = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_PERF_LOG", false); + if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD"))) { Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1"); } - } - - 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(); - - if (Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING) != "1" - // Setting output encoding is not available on those platforms - && UILanguageOverride.OperatingSystemSupportsUtf8()) + // Avoid create temp directory with root permission and later prevent access in non sudo + if (SudoEnvironmentDirectoryOverride.IsRunningUnderSudo()) { - Console.OutputEncoding = Encoding.UTF8; + perfLogEnabled = false; } - DebugHelper.HandleDebugSwitch(ref args); - // By default, .NET Core doesn't have all code pages needed for Console apps. - // See the .NET Core Notes: https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - UILanguageOverride.Setup(); - - var exitCode = 1; - try + PerformanceLogStartupInformation startupInfo = null; + if (perfLogEnabled) { - exitCode = ProcessArgsAndExecute(args); - s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Ok); - return exitCode; + startupInfo = new PerformanceLogStartupInformation(mainTimeStamp); + PerformanceLogManager.InitializeAndStartCleanup(FileSystemWrapper.Default); } - catch (Exception e) when (e.ShouldBeDisplayedAsError()) + + PerformanceLogEventListener perLogEventListener = null; + try { - Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose - ? e.ToString().Red().Bold() - : e.Message.Red().Bold()); + if (perfLogEnabled) + { + perLogEventListener = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory); + } + + PerformanceLogEventSource.Log.LogStartUpInformation(startupInfo); + PerformanceLogEventSource.Log.CLIStart(); - if (e is CommandParsingException { ParseResult: {} exceptionParseResult } ) + InitializeProcess(); + + try { - exceptionParseResult.ShowHelp(); + return ProcessArgs(args, startupTime); + } + catch (Exception e) when (e.ShouldBeDisplayedAsError()) + { + Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose + ? e.ToString().Red().Bold() + : e.Message.Red().Bold()); + + var commandParsingException = e as CommandParsingException; + if (commandParsingException != null && commandParsingException.ParseResult != null) + { + commandParsingException.ParseResult.ShowHelp(); + } + + return 1; + } + catch (Exception e) when (!e.ShouldBeDisplayedAsError()) + { + // If telemetry object has not been initialized yet. It cannot be collected + TelemetryEventEntry.SendFiltered(e); + Reporter.Error.WriteLine(e.ToString().Red().Bold()); + + return 1; + } + finally + { + PerformanceLogEventSource.Log.CLIStop(); } - s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error); - return exitCode; - } - catch (Exception e) when (!e.ShouldBeDisplayedAsError()) - { - TelemetryEventEntry.SendFiltered(e); - Reporter.Error.WriteLine(e.ToString().Red().Bold()); - s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error); - return exitCode; } finally { - TelemetryInstance.TrackEvent("command/finish", new Dictionary { { "exitCode", exitCode.ToString() } }); - Shutdown(default!); - TelemetryClient.WriteLogIfNecessary(); + if (perLogEventListener != null) + { + perLogEventListener.Dispose(); + } } } - internal static int ProcessArgsAndExecute(string[] args) + internal static int ProcessArgs(string[] args) { - ParseResult parseResult = ParseArgs(args); - // Options that perform terminating actions are considered to essentially be subcommands. - // These are special as they should not run the first-run setup. - // Example: dotnet --version - if (!(parseResult.Action is InvocableOptionAction { Terminating: true })) + return ProcessArgs(args, new TimeSpan(0)); + } + + internal static int ProcessArgs(string[] args, TimeSpan startupTime) + { + Dictionary performanceData = []; + + PerformanceLogEventSource.Log.BuiltInCommandParserStart(); + ParseResult parseResult; + using (new PerformanceMeasurement(performanceData, "Parse Time")) { - SetupFirstRun(parseResult); + parseResult = Parser.Parse(args); + + // Avoid create temp directory with root permission and later prevent access in non sudo + // This method need to be run very early before temp folder get created + // https://github.com/dotnet/sdk/issues/20195 + SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult); } + PerformanceLogEventSource.Log.BuiltInCommandParserStop(); - TelemetryEventEntry.SendFiltered(new ParseResultWithGlobalJsonState(parseResult, s_globalJsonState)); - if (parseResult.CanBeInvoked()) + using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel()) { - return ExecuteInternalCommand(parseResult); + IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel; + IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel(); + IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, ToolPathSentinelFileName))); + + PerformanceLogEventSource.Log.TelemetryRegistrationStart(); + + TelemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel); + TelemetryEventEntry.Subscribe(TelemetryClient.TrackEvent); + TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + + PerformanceLogEventSource.Log.TelemetryRegistrationStop(); + + if (parseResult.GetValue(Parser.RootCommand.DiagOption) && parseResult.IsDotnetBuiltInCommand()) + { + // We found --diagnostic or -d, but we still need to determine whether the option should + // be attached to the dotnet command or the subcommand. + if (args.DiagOptionPrecedesSubcommand(parseResult.RootSubCommandResult())) + { + Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString); + CommandLoggingContext.SetVerbose(true); + Reporter.Reset(); + } + } + if (parseResult.HasOption(Parser.RootCommand.VersionOption) && parseResult.IsTopLevelDotnetCommand()) + { + CommandLineInfo.PrintVersion(); + return 0; + } + else if (parseResult.HasOption(Parser.RootCommand.InfoOption) && parseResult.IsTopLevelDotnetCommand()) + { + CommandLineInfo.PrintInfo(); + return 0; + } + else + { + PerformanceLogEventSource.Log.FirstTimeConfigurationStart(); + + var environmentProvider = new EnvironmentProvider(); + + bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true); + bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); + bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true); + bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false); + bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK, + // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false. + defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment()); + + ReportDotnetHomeUsage(environmentProvider); + + var isDotnetBeingInvokedFromNativeInstaller = false; + if (parseResult.CommandResult.Command is InternalReportInstallSuccessCommandDefinition) + { + aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel(); + firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel(); + toolPathSentinel = new NoOpFileSentinel(exists: false); + isDotnetBeingInvokedFromNativeInstaller = true; + } + + var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration( + generateAspNetCertificate: generateAspNetCertificate, + telemetryOptout: telemetryOptout, + addGlobalToolsToPath: addGlobalToolsToPath, + nologo: nologo, + skipWorkloadIntegrityCheck: skipWorkloadIntegrityCheck); + + string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"]; + char[] switchIndicators = ['-', '/']; + var getStarOptionPassed = parseResult.CommandResult.Tokens.Any(t => + getStarOperators.Any(o => + switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase)))); + + ConfigureDotNetForFirstTimeUse( + firstTimeUseNoticeSentinel, + aspNetCertificateSentinel, + toolPathSentinel, + isDotnetBeingInvokedFromNativeInstaller, + dotnetFirstRunConfiguration, + environmentProvider, + performanceData, + skipFirstTimeUseCheck: getStarOptionPassed); + PerformanceLogEventSource.Log.FirstTimeConfigurationStop(); + } } - try + if (CommandLoggingContext.IsVerbose) { - return ExecuteExternalCommand(args, parseResult); + Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}"); } - catch (CommandUnknownException e) + PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart(); + performanceData.Add("Startup Time", startupTime.TotalMilliseconds); + + string globalJsonState = string.Empty; + if (TelemetryClient.Enabled) { - Reporter.Error.WriteLine(e.Message.Red()); - Reporter.Output.WriteLine(e.InstructionMessage); - return 1; + // Get the global.json state to report in telemetry along with this command invocation. + globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory); } - static ParseResult ParseArgs(string[] args) + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData, globalJsonState)); + PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop(); + + int exitCode; + if (parseResult.CanBeInvoked()) + { + InvokeBuiltInCommand(parseResult, out exitCode); + } + else { - ParseResult parseResult; - using (var parseActivity = Activities.Source.StartActivity("parse")) + PerformanceLogEventSource.Log.ExtensibleCommandResolverStart(); + try { - parseResult = Parser.Parse(args); + string commandName = "dotnet-" + parseResult.GetValue(Parser.RootCommand.DotnetSubCommand); + var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec( + new DefaultCommandResolverPolicy(), + commandName, + args.GetSubArguments(), + FrameworkConstants.CommonFrameworks.NetStandardApp15); + + if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode) + { + exitCode = fileBasedAppExitCode; + } + else + { + var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec); + PerformanceLogEventSource.Log.ExtensibleCommandResolverStop(); - // Avoid create temp directory with root permission and later prevent access in non sudo - // This method need to be run very early before temp folder get created - // https://github.com/dotnet/sdk/issues/20195 - SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult); - } - s_mainActivity.SetDisplayName(parseResult); - return parseResult; - } - } + PerformanceLogEventSource.Log.ExtensibleCommandStart(); + var result = resolvedCommand.Execute(); + PerformanceLogEventSource.Log.ExtensibleCommandStop(); - private static void SetupFirstRun(ParseResult parseResult) - { - using var _ = Activities.Source.StartActivity("first-time-use"); - IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel(); - IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel(); - string toolPath = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, s_toolPathSentinelFileName); - IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(toolPath)); - - var environmentProvider = new EnvironmentProvider(); - bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true); - bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); - bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true); - bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false); - bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK, - // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false. - defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment()); - - var isDotnetBeingInvokedFromNativeInstaller = false; - // Note: This should not be special cased like this. Determine if we can skip first run setup entirely for this command. - if (parseResult.CommandResult.Command is InternalReportInstallSuccessCommandDefinition) - { - aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel(); - firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel(); - toolPathSentinel = new NoOpFileSentinel(exists: false); - isDotnetBeingInvokedFromNativeInstaller = true; + exitCode = result.ExitCode; + } + } + catch (CommandUnknownException e) + { + Reporter.Error.WriteLine(e.Message.Red()); + Reporter.Output.WriteLine(e.InstructionMessage); + exitCode = 1; + } } - var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration( - generateAspNetCertificate, - telemetryOptout, - addGlobalToolsToPath, - nologo, - skipWorkloadIntegrityCheck); + TelemetryClient.TrackEvent("command/finish", properties: new Dictionary + { + { "exitCode", exitCode.ToString() } + }, + measurements: new Dictionary()); - string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"]; - char[] switchIndicators = ['-', '/']; - var skipFirstTimeUseCheck = parseResult.CommandResult.Tokens.Any(t => - getStarOperators.Any(o => - switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase)))); + PerformanceLogEventSource.Log.TelemetryClientFlushStart(); + TelemetryClient.Flush(); + PerformanceLogEventSource.Log.TelemetryClientFlushStop(); - var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck; - var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider); - // Note: Not sure why this unused instance type is created. - var __ = new DotNetCommandFactory(alwaysRunOutOfProc: true); - var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator(); - var reporter = Reporter.Error; - var dotnetConfigurer = new DotnetFirstTimeUseConfigurer( - firstTimeUseNoticeSentinel, - aspNetCertificateSentinel, - aspnetCertificateGenerator, - toolPathSentinel, - dotnetFirstRunConfiguration, - reporter, - environmentPath, - skipFirstTimeUseCheck); + TelemetryClient.Dispose(); - dotnetConfigurer.Configure(); + return exitCode; -#if TARGET_WINDOWS - if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows()) + static int? TryRunFileBasedApp(ParseResult parseResult) { - DotDefaultPathCorrector.Correct(); + // If we didn't match any built-in commands, and a C# file path is the first argument, + // parse as `dotnet run --file file.cs ..rest_of_args` instead. + if (parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] } + && VirtualProjectBuilder.IsValidEntryPointPath(unmatchedCommandOrFile.Value)) + { + List otherTokens = new(parseResult.Tokens.Count - 1); + foreach (var token in parseResult.Tokens) + { + if (token != unmatchedCommandOrFile) + { + otherTokens.Add(token.Value); + } + } + + parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile.Value, .. otherTokens]); + + InvokeBuiltInCommand(parseResult, out var exitCode); + return exitCode; + } + + return null; } -#endif - if (isFirstTimeUse && !skipWorkloadIntegrityCheck) + static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode) { + Debug.Assert(parseResult.CanBeInvoked()); + + PerformanceLogEventSource.Log.BuiltInCommandStart(); + try { - WorkloadIntegrityChecker.RunFirstUseCheck(reporter); + exitCode = Parser.Invoke(parseResult); + exitCode = AdjustExitCode(parseResult, exitCode); } - catch (Exception) + catch (Exception exception) { - // If the workload check fails for any reason, we want to eat the failure and continue running the command. - reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow()); + exitCode = Parser.ExceptionHandler(exception, parseResult); } + + PerformanceLogEventSource.Log.BuiltInCommandStop(); } } - private static int ExecuteInternalCommand(ParseResult parseResult) + private static int AdjustExitCode(ParseResult parseResult, int exitCode) { - Debug.Assert(parseResult.CanBeInvoked()); - int exitCode; - using var _ = Activities.Source.StartActivity("invocation"); - try - { - exitCode = Parser.Invoke(parseResult); - if (parseResult.Errors.Any()) - { - exitCode = AdjustExitCodeForNew(); - } - } - catch (Exception exception) - { - exitCode = Parser.ExceptionHandler(exception, parseResult); - } - return exitCode; - - int AdjustExitCodeForNew() + if (parseResult.Errors.Count > 0) { var commandResult = parseResult.CommandResult; + while (commandResult is not null) { if (commandResult.Command.Name == "new") { - // Default parse error exit code is 1. - // For the "new" command and its subcommands, it needs to be 127. + // default parse error exit code is 1 + // for the "new" command and its subcommands it needs to be 127 return 127; } + commandResult = commandResult.Parent as CommandResult; } - return exitCode; } + + return exitCode; } - private static int ExecuteExternalCommand(string[] args, ParseResult parseResult) + private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) { - string commandName = "dotnet-" + parseResult.GetValue(Parser.RootCommand.DotnetSubCommand); - CommandSpec? resolvedCommandSpec = null; - using (var _ = Activities.Source.StartActivity("lookup-external-command")) - { - resolvedCommandSpec = CommandResolver.TryResolveCommandSpec( - new DefaultCommandResolverPolicy(), - commandName, - args.GetSubArguments(), - FrameworkConstants.CommonFrameworks.NetStandardApp15); - } - - if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode) + var home = provider.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName); + if (string.IsNullOrEmpty(home)) { - return fileBasedAppExitCode; + return; } - var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec); - using var __ = Activities.Source.StartActivity("execute-extensible-command"); - return resolvedCommand.Execute().ExitCode; + Reporter.Verbose.WriteLine( + string.Format( + LocalizableStrings.DotnetCliHomeUsed, + home, + CliFolderPathCalculator.DotnetHomeVariableName)); } - private static int? TryRunFileBasedApp(ParseResult parseResult) + private static void ConfigureDotNetForFirstTimeUse( + IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel, + IAspNetCertificateSentinel aspNetCertificateSentinel, + IFileSentinel toolPathSentinel, + bool isDotnetBeingInvokedFromNativeInstaller, + DotnetFirstRunConfiguration dotnetFirstRunConfiguration, + IEnvironmentProvider environmentProvider, + Dictionary performanceMeasurements, + bool skipFirstTimeUseCheck) { - // If we didn't match any built-in commands, and a C# file path is the first argument, - // parse as `dotnet run file.cs ..rest_of_args` instead. - if (parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] } - && VirtualProjectBuilder.IsValidEntryPointPath(unmatchedCommandOrFile.Value)) + var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck; + var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider); + _ = new DotNetCommandFactory(alwaysRunOutOfProc: true); + var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator(); + var reporter = Reporter.Error; + var dotnetConfigurer = new DotnetFirstTimeUseConfigurer( + firstTimeUseNoticeSentinel, + aspNetCertificateSentinel, + aspnetCertificateGenerator, + toolPathSentinel, + dotnetFirstRunConfiguration, + reporter, + environmentPath, + performanceMeasurements, + skipFirstTimeUseCheck: skipFirstTimeUseCheck); + + dotnetConfigurer.Configure(); + +#if TARGET_WINDOWS + if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows()) + { + DotDefaultPathCorrector.Correct(); + } +#endif + + if (isFirstTimeUse && !dotnetFirstRunConfiguration.SkipWorkloadIntegrityCheck) { - List otherTokens = new(parseResult.Tokens.Count - 1); - foreach (var token in parseResult.Tokens) + try { - if (token.Type != TokenType.Argument || token != unmatchedCommandOrFile) - { - otherTokens.Add(token.Value); - } + WorkloadIntegrityChecker.RunFirstUseCheck(reporter); + } + catch (Exception) + { + // If the workload check fails for any reason, we want to eat the failure and continue running the command. + reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow()); } - parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile.Value, .. otherTokens]); - return ExecuteInternalCommand(parseResult); } - - return null; } - public static void Shutdown(PosixSignalContext context) + private static void InitializeProcess() { - s_sigIntRegistration.Dispose(); - s_sigQuitRegistration.Dispose(); - s_sigTermRegistration.Dispose(); - s_mainActivity?.Stop(); - TelemetryClient.FlushProviders(); - Activities.Source.Dispose(); + // by default, .NET Core doesn't have all code pages needed for Console apps. + // see the .NET Core Notes in https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + UILanguageOverride.Setup(); } } diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs b/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs index 86e124e8e668..abe2058763ff 100644 --- a/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs +++ b/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs @@ -14,10 +14,10 @@ internal class AllowListToSendFirstAppliedOptions( { private HashSet _topLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList; - public List AllowList(ParseResult parseResult) + public List AllowList(ParseResult parseResult, Dictionary measurements = null) { var topLevelCommandNameFromParse = parseResult.RootSubCommandResult(); - var result = new List(); + var result = new List(); if (_topLevelCommandNameAllowList.Contains(topLevelCommandNameFromParse)) { var firstOption = parseResult.RootCommandResult.Children @@ -25,13 +25,14 @@ public List AllowList(ParseResult parseResult) .Children.OfType().FirstOrDefault()?.Command.Name ?? null; if (firstOption != null) { - result.Add(new TelemetryEntryFormat( + result.Add(new ApplicationInsightsEntryFormat( "sublevelparser/command", new Dictionary { - {"verb", topLevelCommandNameFromParse}, + { "verb", topLevelCommandNameFromParse}, {"argument", firstOption} - })); + }, + measurements)); } } return result; diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs b/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs index 4961193ca740..0b303a14bcd7 100644 --- a/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs +++ b/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs @@ -1,19 +1,22 @@ // 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 System.CommandLine; using System.CommandLine.Parsing; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Telemetry; -internal class AllowListToSendFirstArgument(HashSet topLevelCommandNameAllowList) : IParseResultLogRule +internal class AllowListToSendFirstArgument( + HashSet topLevelCommandNameAllowList) : IParseResultLogRule { private HashSet _topLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList; - public List AllowList(ParseResult parseResult) + public List AllowList(ParseResult parseResult, Dictionary measurements = null) { - var result = new List(); + var result = new List(); var topLevelCommandNameFromParse = parseResult.RootCommandResult.Children.FirstOrDefault() switch { System.CommandLine.Parsing.CommandResult commandResult => commandResult.Command.Name, @@ -25,17 +28,17 @@ public List AllowList(ParseResult parseResult) { if (_topLevelCommandNameAllowList.Contains(topLevelCommandNameFromParse)) { - var firstArgument = parseResult.RootCommandResult.Children.FirstOrDefault()?.Tokens - .Where(t => t.Type.Equals(TokenType.Argument)).FirstOrDefault()?.Value ?? null; + var firstArgument = parseResult.RootCommandResult.Children.FirstOrDefault()?.Tokens.Where(t => t.Type.Equals(TokenType.Argument)).FirstOrDefault()?.Value ?? null; if (firstArgument != null) { - result.Add(new TelemetryEntryFormat( + result.Add(new ApplicationInsightsEntryFormat( "sublevelparser/command", - new Dictionary + new Dictionary { {"verb", topLevelCommandNameFromParse}, {"argument", firstArgument} - })); + }, + measurements)); } } } diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs b/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs index 7159b126ac2a..44eff7eb5707 100644 --- a/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs +++ b/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.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. +#nullable disable + using System.CommandLine; using System.CommandLine.Parsing; using Microsoft.DotNet.Cli.Extensions; @@ -8,13 +10,14 @@ namespace Microsoft.DotNet.Cli.Telemetry; -internal class AllowListToSendVerbSecondVerbFirstArgument(HashSet topLevelCommandNameAllowList) : IParseResultLogRule +internal class AllowListToSendVerbSecondVerbFirstArgument( + HashSet topLevelCommandNameAllowList) : IParseResultLogRule { private HashSet TopLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList; - public List AllowList(ParseResult parseResult) + public List AllowList(ParseResult parseResult, Dictionary measurements = null) { - var result = new List(); + var result = new List(); var topLevelCommandNameFromParse = parseResult.RootSubCommandResult(); if (topLevelCommandNameFromParse != null) @@ -26,14 +29,15 @@ public List AllowList(ParseResult parseResult) var firstArgument = parseResult.Tokens.FirstOrDefault(t => t.Type.Equals(TokenType.Argument))?.Value ?? ""; if (secondVerb != null) { - result.Add(new TelemetryEntryFormat( + result.Add(new ApplicationInsightsEntryFormat( "sublevelparser/command", - new Dictionary + new Dictionary { {"verb", topLevelCommandNameFromParse}, {"subcommand", secondVerb}, {"argument", firstArgument} - })); + }, + measurements)); } } } diff --git a/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs b/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs index b1de4ad11aa7..a333f4127416 100644 --- a/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs +++ b/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.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. +#nullable disable + using System.Diagnostics; using System.Globalization; using System.Security; @@ -16,7 +18,7 @@ internal static class ExternalTelemetryProperties /// For Windows, returns the OS installation type, eg. "Nano Server", "Server Core", "Server", or "Client". /// For Unix, or on error, currently returns empty string. /// - internal static string? GetInstallationType() + internal static string GetInstallationType() { if (!OperatingSystem.IsWindows()) { @@ -28,7 +30,7 @@ internal static class ExternalTelemetryProperties try { - return (string?)Registry.GetValue(Key, ValueName, defaultValue: ""); + return (string)Registry.GetValue(Key, ValueName, defaultValue: ""); } // Catch everything: this is for telemetry only. catch (Exception e) @@ -48,7 +50,7 @@ internal static class ExternalTelemetryProperties /// We're not attempting to decode the value on the client side as new Windows releases may add new values. /// For Unix, or on error, returns an empty string. /// - internal static string? GetProductType() + internal static string GetProductType() { if (!OperatingSystem.IsWindows()) { @@ -82,7 +84,7 @@ internal static class ExternalTelemetryProperties /// If the libc is musl, currently returns empty string. /// Otherwise returns empty string. /// - internal static string? GetLibcRelease() + internal static string GetLibcRelease() { if (OperatingSystem.IsWindows()) { @@ -106,7 +108,7 @@ internal static class ExternalTelemetryProperties /// If the libc is musl, currently returns empty string. (In future could run "ldd -version".) /// Otherwise returns empty string. /// - internal static string? GetLibcVersion() + internal static string GetLibcVersion() { if (OperatingSystem.IsWindows()) { diff --git a/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs b/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs index 7dfe292c6c0c..6384f7dbd59a 100644 --- a/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs +++ b/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs @@ -10,5 +10,5 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal interface IParseResultLogRule { - List AllowList(ParseResult parseResult); + List AllowList(ParseResult parseResult, Dictionary measurements = null); } diff --git a/src/Cli/dotnet/Telemetry/ITelemetryClient.cs b/src/Cli/dotnet/Telemetry/ITelemetry.cs similarity index 65% rename from src/Cli/dotnet/Telemetry/ITelemetryClient.cs rename to src/Cli/dotnet/Telemetry/ITelemetry.cs index f022648a59ff..b8ee7c98e118 100644 --- a/src/Cli/dotnet/Telemetry/ITelemetryClient.cs +++ b/src/Cli/dotnet/Telemetry/ITelemetry.cs @@ -1,11 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + namespace Microsoft.DotNet.Cli.Telemetry; -public interface ITelemetryClient +public interface ITelemetry { bool Enabled { get; } - void TrackEvent(string eventName, IDictionary? properties); + void TrackEvent(string eventName, IDictionary properties, IDictionary measurements); + + void Flush(); + + void Dispose(); } diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs new file mode 100644 index 000000000000..2966dfc205a8 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs @@ -0,0 +1,61 @@ +// 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.ApplicationInsights.Channel; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal abstract class BaseStorageService +{ + /// + /// Peeked transmissions dictionary (maps file name to its full path). Holds all the transmissions that were peeked. + /// + /// + /// Note: The value (=file's full path) is not required in the Storage implementation. + /// If there was a concurrent Abstract Data Type Set it would have been used instead. + /// However, since there is no concurrent Set, dictionary is used and the second value is ignored. + /// + protected IDictionary PeekedTransmissions; + + /// + /// Gets or sets the maximum size of the storage in bytes. When limit is reached, the Enqueue method will drop new + /// transmissions. + /// + internal ulong CapacityInBytes { get; set; } + + /// + /// Gets or sets the maximum number of files. When limit is reached, the Enqueue method will drop new transmissions. + /// + internal uint MaxFiles { get; set; } + + internal abstract string StorageDirectoryPath { get; } + + /// + /// Initializes the + /// + /// A folder name. Under this folder all the transmissions will be saved. + internal abstract void Init(string desireStorageDirectoryPath); + + internal abstract StorageTransmission Peek(); + + internal abstract void Delete(StorageTransmission transmission); + + internal abstract Task EnqueueAsync(Transmission transmission); + + protected void OnPeekedItemDisposed(string fileName) + { + try + { + if (PeekedTransmissions.ContainsKey(fileName)) + { + PeekedTransmissions.Remove(fileName); + } + } + catch (Exception e) + { + PersistenceChannelDebugLog.WriteException(e, "Failed to remove the item from storage items."); + } + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs new file mode 100644 index 000000000000..4a80be224e9c --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +/// +/// A light fixed size queue. If Enqueue is called and queue's limit has reached the last item will be removed. +/// This data structure is thread safe. +/// +internal class FixedSizeQueue +{ + private readonly int _maxSize; + private readonly Queue _queue = new(); + private readonly object _queueLockObj = new(); + + internal FixedSizeQueue(int maxSize) + { + _maxSize = maxSize; + } + + internal void Enqueue(T item) + { + lock (_queueLockObj) + { + if (_queue.Count == _maxSize) + { + _queue.Dequeue(); + } + + _queue.Enqueue(item); + } + } + + internal bool Contains(T item) + { + lock (_queueLockObj) + { + return _queue.Contains(item); + } + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs new file mode 100644 index 000000000000..8d14b740cca0 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs @@ -0,0 +1,57 @@ +// 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.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.Extensibility.Implementation; +using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +/// +/// This class handles all the logic for flushing the In Memory buffer to the persistent storage. +/// +internal class FlushManager +{ + /// + /// The storage that is used to persist all the transmissions. + /// + private readonly BaseStorageService _storage; + + /// + /// Initializes a new instance of the class. + /// + /// The storage that persists the telemetries. + internal FlushManager(BaseStorageService storage) + { + _storage = storage; + } + + /// + /// Gets or sets the service endpoint. + /// + /// + /// Q: Why flushManager knows about the endpoint? + /// A: Storage stores Transmission objects and Transmission objects contain the endpoint address. + /// + internal Uri EndpointAddress { get; set; } + + /// + /// Persist the in-memory telemetry items. + /// + internal void Flush(IChannelTelemetry telemetryItem) + { + if (telemetryItem != null) + { + byte[] data = JsonSerializer.Serialize([telemetryItem]); + Transmission transmission = new( + EndpointAddress, + data, + "application/x-json-stream", + JsonSerializer.CompressionType); + + _storage.EnqueueAsync(transmission).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs new file mode 100644 index 000000000000..69affdc523a8 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs @@ -0,0 +1,114 @@ +// 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.ApplicationInsights.Channel; +using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +/// +/// Represents a communication channel for sending telemetry to Application Insights via HTTPS. +/// +internal sealed class PersistenceChannel : ITelemetryChannel +{ + internal const string TelemetryServiceEndpoint = "https://dc.services.visualstudio.com/v2/track"; + + private readonly FlushManager _flushManager; + + private int _disposeCount; + private readonly BaseStorageService _storage; + private readonly PersistenceTransmitter _transmitter; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Full path of a directory name. Under this folder all the transmissions will be saved. + /// Setting this value groups channels, even from different processes. + /// If 2 (or more) channels has the same storageFolderName only one channel will perform the sending even if the + /// channel is in a different process/AppDomain/Thread. + /// + /// + /// Defines the number of senders. A sender is a long-running thread that sends telemetry batches in intervals defined + /// by . + /// So the amount of senders also defined the maximum amount of http channels opened at the same time. + /// + public PersistenceChannel(string storageDirectoryPath = null, int sendersCount = 1) + { + _storage = new StorageService(); + _storage.Init(storageDirectoryPath); + _transmitter = new PersistenceTransmitter(_storage, sendersCount); + _flushManager = new FlushManager(_storage); + EndpointAddress = TelemetryServiceEndpoint; + } + + /// + /// Gets or sets an interval between each successful sending. + /// + /// + /// On error scenario this value is ignored and the interval will be defined using an exponential back-off + /// algorithm. + /// + public TimeSpan? SendingInterval + { + get => _transmitter.SendingInterval; + set => _transmitter.SendingInterval = value; + } + + + /// + /// Gets or sets the maximum amount of files allowed in storage. When the limit is reached telemetries will be dropped. + /// + public uint MaxTransmissionStorageFilesCapacity + { + get => _storage.MaxFiles; + set => _storage.MaxFiles = value; + } + + /// + /// This flag has no effect. But it is required by base class + /// + public bool? DeveloperMode { get; set; } + + /// + /// Gets or sets the HTTP address where the telemetry is sent. + /// + public string EndpointAddress + { + get => _flushManager.EndpointAddress.ToString(); + + set + { + string address = value ?? TelemetryServiceEndpoint; + _flushManager.EndpointAddress = new Uri(address); + } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + if (Interlocked.Increment(ref _disposeCount) == 1) + { + _transmitter?.Dispose(); + } + } + + /// + /// Sends an instance of ITelemetry through the channel. + /// + public void Send(IChannelTelemetry item) + { + _flushManager.Flush(item); + } + + /// + /// No operation, send will always flush. So nothing will be in memory + /// + public void Flush() + { + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs new file mode 100644 index 000000000000..ff695b79e6c3 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.Globalization; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal static class PersistenceChannelDebugLog +{ + private static readonly bool _isEnabled = IsEnabledByEnvironment(); + + private static bool IsEnabledByEnvironment() + { + var environmentProvider = new EnvironmentProvider(); + return environmentProvider.GetEnvironmentVariableAsBool("DOTNET_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT", false); + } + + public static void WriteLine(string message) + { + if (_isEnabled) + { + Reporter.Output.WriteLine(message); + } + } + + internal static void WriteException(Exception exception, string format, params string[] args) + { + var message = string.Format(CultureInfo.InvariantCulture, format, args); + WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} Exception: {1}", message, exception.ToString())); + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs new file mode 100644 index 000000000000..0cede405eb04 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +/// +/// Implements throttled and persisted transmission of telemetry to Application Insights. +/// +internal class PersistenceTransmitter : IDisposable +{ + /// + /// The number of times this object was disposed. + /// + private int _disposeCount; + + /// + /// A list of senders that sends transmissions. + /// + private readonly List _senders = []; + + /// + /// The storage that is used to persist all the transmissions. + /// + private readonly BaseStorageService _storage; + + /// + /// Initializes a new instance of the class. + /// + /// The transmissions storage. + /// The number of senders to create. + /// + /// A boolean value that indicates if this class should try and create senders. This is a + /// workaround for unit tests purposes only. + /// + internal PersistenceTransmitter(BaseStorageService storage, int sendersCount, bool createSenders = true) + { + _storage = storage; + if (createSenders) + { + for (int i = 0; i < sendersCount; i++) + { + _senders.Add(new Sender(_storage, this)); + } + } + } + + /// + /// Gets or sets the interval between each successful sending. + /// + internal TimeSpan? SendingInterval { get; set; } + + /// + /// Disposes the object. + /// + public void Dispose() + { + if (Interlocked.Increment(ref _disposeCount) == 1) + { + StopSenders(); + } + } + + /// + /// Stops the senders. + /// + /// As long as there is no Start implementation, this method should only be called from Dispose. + private void StopSenders() + { + if (_senders == null) + { + return; + } + + List stoppedTasks = []; + foreach (Sender sender in _senders) + { + stoppedTasks.Add(sender.StopAsync()); + } + + Task.WaitAll([.. stoppedTasks]); + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs new file mode 100644 index 000000000000..6e0f7ceceaa9 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs @@ -0,0 +1,336 @@ +// 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 System.Net; +using System.Net.NetworkInformation; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +/// +/// Fetch transmissions from the storage and sends it. +/// +internal class Sender : IDisposable +{ + /// + /// The default sending interval. + /// + private readonly TimeSpan _defaultSendingInterval; + + /// + /// A wait handle that flags the sender when to start sending again. The type is protected for unit test. + /// + protected readonly AutoResetEvent DelayHandler; + + /// + /// Holds the maximum time for the exponential back-off algorithm. The sending interval will grow on every HTTP + /// Exception until this max value. + /// + private readonly TimeSpan _maxIntervalBetweenRetries = TimeSpan.FromHours(1); + + /// + /// When storage is empty it will be queried again after this interval. + /// Decreasing to 5 sec to send first data (users and sessions). + /// + private readonly TimeSpan _sendingIntervalOnNoData = TimeSpan.FromSeconds(5); + + /// + /// A wait handle that is being set when Sender is no longer sending. + /// + private readonly AutoResetEvent _stoppedHandler; + + /// + /// The number of times this object was disposed. + /// + private int _disposeCount; + + /// + /// The amount of time to wait, in the stop method, until the last transmission is sent. + /// If time expires, the stop method will return even if the transmission hasn't been sent. + /// + private readonly TimeSpan _drainingTimeout; + + /// + /// A boolean value that indicates if the sender should be stopped. The sender's while loop is checking this boolean + /// value. + /// + private bool _stopped; + + /// + /// The transmissions storage. + /// + private readonly BaseStorageService _storage; + + /// + /// Holds the transmitter. + /// + private readonly PersistenceTransmitter _transmitter; + + /// + /// Initializes a new instance of the class. + /// + /// The storage that holds the transmissions to send. + /// + /// The persistence transmitter that manages this Sender. + /// The transmitter will be used as a configuration class, it exposes properties like SendingInterval that will be read + /// by Sender. + /// + /// + /// A boolean value that determines if Sender should start sending immediately. This is only + /// used for unit tests. + /// + internal Sender(BaseStorageService storage, PersistenceTransmitter transmitter, bool startSending = true) + { + _stopped = false; + DelayHandler = new AutoResetEvent(false); + _stoppedHandler = new AutoResetEvent(false); + _drainingTimeout = TimeSpan.FromSeconds(100); + _defaultSendingInterval = TimeSpan.FromSeconds(5); + + _transmitter = transmitter; + _storage = storage; + + if (startSending) + { + // It is currently possible for the long - running task to be executed(and thereby block during WaitOne) on the UI thread when + // called by a task scheduled on the UI thread. Explicitly specifying TaskScheduler.Default + // when calling StartNew guarantees that Sender never blocks the main thread. + Task.Factory.StartNew(SendLoop, CancellationToken.None, TaskCreationOptions.LongRunning, + TaskScheduler.Default) + .ContinueWith( + t => PersistenceChannelDebugLog.WriteException(t.Exception, "Sender: Failure in SendLoop"), + TaskContinuationOptions.OnlyOnFaulted); + } + } + + /// + /// Gets the interval between each successful sending. + /// + private TimeSpan SendingInterval + { + get + { + if (_transmitter.SendingInterval != null) + { + return _transmitter.SendingInterval.Value; + } + + return _defaultSendingInterval; + } + } + + /// + /// Disposes the managed objects. + /// + public void Dispose() + { + if (Interlocked.Increment(ref _disposeCount) == 1) + { + StopAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + DelayHandler.Dispose(); + _stoppedHandler.Dispose(); + } + } + + /// + /// Stops the sender. + /// + internal Task StopAsync() + { + // After delayHandler is set, a sending iteration will immediately start. + // Setting stopped to true, will cause the iteration to skip the actual sending and stop immediately. + _stopped = true; + DelayHandler.Set(); + + // if delayHandler was set while a transmission was being sent, the return task will wait for it to finish, for an additional second, + // before it will mark the task as completed. + return Task.Run(() => + { + try + { + _stoppedHandler.WaitOne(_drainingTimeout); + } + catch (ObjectDisposedException) + { + } + }); + } + + /// + /// Send transmissions in a loop. + /// + protected void SendLoop() + { + TimeSpan prevSendingInterval = TimeSpan.Zero; + TimeSpan sendingInterval = _sendingIntervalOnNoData; + try + { + while (!_stopped) + { + using (StorageTransmission transmission = _storage.Peek()) + { + if (_stopped) + { + // This second verification is required for cases where 'stopped' was set while peek was happening. + // Once the actual sending starts the design is to wait until it finishes and deletes the transmission. + // So no extra validation is required. + break; + } + + // If there is a transmission to send - send it. + if (transmission != null) + { + bool shouldRetry = Send(transmission, ref sendingInterval); + if (!shouldRetry) + { + // If retry is not required - delete the transmission. + _storage.Delete(transmission); + } + } + else + { + sendingInterval = _sendingIntervalOnNoData; + } + } + + LogInterval(prevSendingInterval, sendingInterval); + DelayHandler.WaitOne(sendingInterval); + prevSendingInterval = sendingInterval; + } + + _stoppedHandler.Set(); + } + catch (ObjectDisposedException) + { + } + } + + /// + /// Sends a transmission and handle errors. + /// + /// The transmission to send. + /// + /// When this value returns it will hold a recommendation for when to start the next sending + /// iteration. + /// + /// True, if there was sent error and we need to retry sending, otherwise false. + protected virtual bool Send(StorageTransmission transmission, ref TimeSpan nextSendInterval) + { + try + { + if (transmission != null) + { + bool isConnected = NetworkInterface.GetIsNetworkAvailable(); + + // there is no internet connection available, return than. + if (!isConnected) + { + PersistenceChannelDebugLog.WriteLine( + "Cannot send data to the server. Internet connection is not available"); + return true; + } + + transmission.SendAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + // After a successful sending, try immediately to send another transmission. + nextSendInterval = SendingInterval; + } + } + catch (WebException e) + { + int? statusCode = GetStatusCode(e); + nextSendInterval = CalculateNextInterval(statusCode, nextSendInterval, _maxIntervalBetweenRetries); + return IsRetryable(statusCode, e.Status); + } + catch (Exception e) + { + nextSendInterval = CalculateNextInterval(null, nextSendInterval, _maxIntervalBetweenRetries); + PersistenceChannelDebugLog.WriteException(e, "Unknown exception during sending"); + } + + return false; + } + + /// + /// Log next interval. Only log the interval when it changes by more then a minute. So if interval grow by 1 minute or + /// decreased by 1 minute it will be logged. + /// Logging every interval will just make the log noisy. + /// + private static void LogInterval(TimeSpan prevSendInterval, TimeSpan nextSendInterval) + { + if (Math.Abs(nextSendInterval.TotalSeconds - prevSendInterval.TotalSeconds) > 60) + { + PersistenceChannelDebugLog.WriteLine("next sending interval: " + nextSendInterval); + } + } + + /// + /// Return the status code from the web exception or null if no such code exists. + /// + private static int? GetStatusCode(WebException e) + { + if (e.Response is HttpWebResponse httpWebResponse) + { + return (int)httpWebResponse.StatusCode; + } + + return null; + } + + /// + /// Returns true if or are retryable. + /// + private static bool IsRetryable(int? httpStatusCode, WebExceptionStatus webExceptionStatus) + { + switch (webExceptionStatus) + { + case WebExceptionStatus.ProxyNameResolutionFailure: + case WebExceptionStatus.NameResolutionFailure: + case WebExceptionStatus.Timeout: + case WebExceptionStatus.ConnectFailure: + return true; + } + + if (httpStatusCode == null) + { + return false; + } + + switch (httpStatusCode.Value) + { + case 503: // Server in maintenance. + case 408: // invalid request + case 500: // Internal Server Error + case 502: // Bad Gateway, can be common when there is no network. + case 511: // Network Authentication Required + return true; + } + + return false; + } + + /// + /// Calculates the next interval using exponential back-off algorithm (with the exceptions of few error codes that + /// reset the interval to . + /// + private TimeSpan CalculateNextInterval(int? httpStatusCode, TimeSpan currentSendInterval, TimeSpan maxInterval) + { + // if item is expired, no need for exponential back-off + if (httpStatusCode != null && httpStatusCode.Value == 400 /* expired */) + { + return SendingInterval; + } + + // exponential back-off. + if (Math.Abs(currentSendInterval.TotalSeconds) < 1) + { + return TimeSpan.FromSeconds(1); + } + + double nextIntervalInSeconds = Math.Min(currentSendInterval.TotalSeconds * 2, maxInterval.TotalSeconds); + + return TimeSpan.FromSeconds(nextIntervalInSeconds); + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs new file mode 100644 index 000000000000..3e98a0d598a8 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.Collections; +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal abstract class SnapshottingCollection : ICollection + where TCollection : class, ICollection +{ + protected readonly TCollection Collection; + protected TCollection snapshot; + + protected SnapshottingCollection(TCollection collection) + { + Debug.Assert(collection != null, "collection"); + Collection = collection; + } + + public int Count => GetSnapshot().Count; + + public bool IsReadOnly => false; + + public void Add(TItem item) + { + lock (Collection) + { + Collection.Add(item); + snapshot = default; + } + } + + public void Clear() + { + lock (Collection) + { + Collection.Clear(); + snapshot = default; + } + } + + public bool Contains(TItem item) + { + return GetSnapshot().Contains(item); + } + + public void CopyTo(TItem[] array, int arrayIndex) + { + GetSnapshot().CopyTo(array, arrayIndex); + } + + public bool Remove(TItem item) + { + lock (Collection) + { + bool removed = Collection.Remove(item); + if (removed) + { + snapshot = default; + } + + return removed; + } + } + + public IEnumerator GetEnumerator() + { + return GetSnapshot().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + protected abstract TCollection CreateSnapshot(TCollection collection); + + protected TCollection GetSnapshot() + { + TCollection localSnapshot = snapshot; + if (localSnapshot == null) + { + lock (Collection) + { + snapshot = CreateSnapshot(Collection); + localSnapshot = snapshot; + } + } + + return localSnapshot; + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs new file mode 100644 index 000000000000..cc6cc51a1f57 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal class SnapshottingDictionary : + SnapshottingCollection, IDictionary>, IDictionary +{ + public SnapshottingDictionary() + : base(new Dictionary()) + { + } + + public ICollection Keys => GetSnapshot().Keys; + + public ICollection Values => GetSnapshot().Values; + + public TValue this[TKey key] + { + get => GetSnapshot()[key]; + + set + { + lock (Collection) + { + Collection[key] = value; + snapshot = null; + } + } + } + + public void Add(TKey key, TValue value) + { + lock (Collection) + { + Collection.Add(key, value); + snapshot = null; + } + } + + public bool ContainsKey(TKey key) + { + return GetSnapshot().ContainsKey(key); + } + + public bool Remove(TKey key) + { + lock (Collection) + { + bool removed = Collection.Remove(key); + if (removed) + { + snapshot = null; + } + + return removed; + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + return GetSnapshot().TryGetValue(key, out value); + } + + protected sealed override IDictionary CreateSnapshot(IDictionary collection) + { + return new Dictionary(collection); + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs new file mode 100644 index 000000000000..dc44a0bf3cbb --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs @@ -0,0 +1,352 @@ +// 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 System.Globalization; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.DotNet.Configurer; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal sealed class StorageService : BaseStorageService +{ + private const string DefaultStorageFolderName = "TelemetryStorageService"; + private readonly FixedSizeQueue _deletedFilesQueue = new(10); + + private readonly object _peekLockObj = new(); + private readonly object _storageFolderLock = new(); + private string _storageDirectoryPath; + private string _storageDirectoryPathUsed; + private long _storageCountFiles; + private bool _storageFolderInitialized; + private long _storageSize; + private uint _transmissionsDropped; + + /// + /// Gets the storage's folder name. + /// + internal override string StorageDirectoryPath => _storageDirectoryPath; + + /// + /// Gets the storage folder. If storage folder couldn't be created, null will be returned. + /// + private string StorageFolder + { + get + { + if (!_storageFolderInitialized) + { + lock (_storageFolderLock) + { + if (!_storageFolderInitialized) + { + try + { + _storageDirectoryPathUsed = _storageDirectoryPath; + + if (!Directory.Exists(_storageDirectoryPathUsed)) + { + Directory.CreateDirectory(_storageDirectoryPathUsed); + } + } + catch (Exception e) + { + _storageDirectoryPathUsed = null; + PersistenceChannelDebugLog.WriteException(e, "Failed to create storage folder"); + } + + _storageFolderInitialized = true; + } + } + } + + return _storageDirectoryPathUsed; + } + } + + internal override void Init(string storageDirectoryPath) + { + PeekedTransmissions = new SnapshottingDictionary(); + + VerifyOrSetDefaultStorageDirectoryPath(storageDirectoryPath); + + CapacityInBytes = 10 * 1024 * 1024; // 10 MB + MaxFiles = 100; + + Task.Run(DeleteObsoleteFiles) + .ContinueWith( + task => + { + PersistenceChannelDebugLog.WriteException( + task.Exception, + "Storage: Unhandled exception in DeleteObsoleteFiles"); + }, + TaskContinuationOptions.OnlyOnFaulted); + } + + private void VerifyOrSetDefaultStorageDirectoryPath(string desireStorageDirectoryPath) + { + if (string.IsNullOrEmpty(desireStorageDirectoryPath)) + { + _storageDirectoryPath = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, + DefaultStorageFolderName); + } + else + { + if (!Path.IsPathRooted(desireStorageDirectoryPath)) + { + throw new ArgumentException($"{nameof(desireStorageDirectoryPath)} need to be rooted (full path)"); + } + + _storageDirectoryPath = desireStorageDirectoryPath; + } + } + + /// + /// Reads an item from the storage. Order is Last-In-First-Out. + /// When the Transmission is no longer needed (it was either sent or failed with a non-retryable error) it should be + /// disposed. + /// + internal override StorageTransmission Peek() + { + IEnumerable files = GetFiles("*.trn", 50); + + lock (_peekLockObj) + { + foreach (string file in files) + { + try + { + // if a file was peeked before, skip it (wait until it is disposed). + if (PeekedTransmissions.ContainsKey(file) == false && + _deletedFilesQueue.Contains(file) == false) + { + // Load the transmission from disk. + StorageTransmission storageTransmissionItem = LoadTransmissionFromFileAsync(file) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + // when item is disposed it should be removed from the peeked list. + storageTransmissionItem.Disposing = item => OnPeekedItemDisposed(file); + + // add the transmission to the list. + PeekedTransmissions.Add(file, storageTransmissionItem.FullFilePath); + return storageTransmissionItem; + } + } + catch (Exception e) + { + PersistenceChannelDebugLog.WriteException( + e, + "Failed to load an item from the storage. file: {0}", + file); + } + } + } + + return null; + } + + internal override void Delete(StorageTransmission item) + { + try + { + if (StorageFolder == null) + { + return; + } + + // Initial storage size calculation. + CalculateSize(); + + long fileSize = GetSize(item.FileName); + File.Delete(Path.Combine(StorageFolder, item.FileName)); + + _deletedFilesQueue.Enqueue(item.FileName); + + // calculate size + Interlocked.Add(ref _storageSize, -fileSize); + Interlocked.Decrement(ref _storageCountFiles); + } + catch (IOException e) + { + PersistenceChannelDebugLog.WriteException(e, "Failed to delete a file. file: {0}", item == null ? "null" : item.FullFilePath); + } + } + + internal override async Task EnqueueAsync(Transmission transmission) + { + try + { + if (transmission == null || StorageFolder == null) + { + return; + } + + // Initial storage size calculation. + CalculateSize(); + + if ((ulong)_storageSize >= CapacityInBytes || _storageCountFiles >= MaxFiles) + { + // if max storage capacity has reached, drop the transmission (but log every 100 lost transmissions). + if (_transmissionsDropped++ % 100 == 0) + { + PersistenceChannelDebugLog.WriteLine("Total transmissions dropped: " + _transmissionsDropped); + } + + return; + } + + // Writes content to a temporary file and only then rename to avoid the Peek from reading the file before it is being written. + // Creates the temp file name + string tempFileName = Guid.NewGuid().ToString("N"); + + // Now that the file got created we can increase the files count + Interlocked.Increment(ref _storageCountFiles); + + // Saves transmission to the temp file + await SaveTransmissionToFileAsync(transmission, tempFileName).ConfigureAwait(false); + + // Now that the file is written increase storage size. + long temporaryFileSize = GetSize(tempFileName); + Interlocked.Add(ref _storageSize, temporaryFileSize); + + // Creates a new file name + string now = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); + string newFileName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}.trn", now, tempFileName); + + // Renames the file + File.Move(Path.Combine(StorageFolder, tempFileName), Path.Combine(StorageFolder, newFileName)); + } + catch (Exception e) + { + PersistenceChannelDebugLog.WriteException(e, "EnqueueAsync"); + } + } + + private async Task SaveTransmissionToFileAsync(Transmission transmission, string file) + { + try + { + using (Stream stream = File.OpenWrite(Path.Combine(StorageFolder, file))) + { + await StorageTransmission.SaveAsync(transmission, stream).ConfigureAwait(false); + } + } + catch (UnauthorizedAccessException) + { + string message = + string.Format( + "Failed to save transmission to file. UnauthorizedAccessException. File path: {0}, FileName: {1}", + StorageFolder, file); + PersistenceChannelDebugLog.WriteLine(message); + throw; + } + } + + private async Task LoadTransmissionFromFileAsync(string file) + { + try + { + using (Stream stream = File.OpenRead(Path.Combine(StorageFolder, file))) + { + StorageTransmission storageTransmissionItem = + await StorageTransmission.CreateFromStreamAsync(stream, file).ConfigureAwait(false); + return storageTransmissionItem; + } + } + catch (Exception e) + { + string message = + string.Format( + "Failed to load transmission from file. File path: {0}, FileName: {1}, Exception: {2}", + "storageFolderName", file, e); + PersistenceChannelDebugLog.WriteLine(message); + throw; + } + } + + /// + /// Get files from . + /// + /// Define the logic for sorting the files. + /// Defines a file extension. This method will return only files with this extension. + /// + /// Define how many files to return. This can be useful when the directory has a lot of files, in that case + /// GetFilesAsync will have a performance hit. + /// + private IEnumerable GetFiles(string filterByExtension, int top) + { + try + { + if (StorageFolder != null) + { + return Directory.GetFiles(StorageFolder, filterByExtension).Take(top); + } + } + catch (Exception e) + { + PersistenceChannelDebugLog.WriteException(e, "Peek failed while get files from storage."); + } + + return []; + } + + /// + /// Gets a file's size. + /// + private long GetSize(string file) + { + using (FileStream stream = File.OpenRead(Path.Combine(StorageFolder, file))) + { + return stream.Length; + } + } + + /// + /// Check the storage limits and return true if they reached. + /// Storage limits are defined by the number of files and the total size on disk. + /// + private void CalculateSize() + { + string[] storageFiles = Directory.GetFiles(StorageFolder, "*.*"); + + _storageCountFiles = storageFiles.Count(); + + long storageSizeInBytes = 0; + foreach (string file in storageFiles) + { + storageSizeInBytes += GetSize(file); + } + + _storageSize = storageSizeInBytes; + } + + /// + /// Enqueue is saving a transmission to a file with a guid, and after a successful write operation it renames it to a + /// trn file. + /// A file without a trn extension is ignored by Storage.Peek(), so if a process is taken down before rename + /// happens it will stay on the disk forever. + /// This thread deletes files with the trn extension that exists on disk for more than 5 minutes. + /// + private void DeleteObsoleteFiles() + { + try + { + IEnumerable files = GetFiles("*.trn", 50); + foreach (string file in files) + { + DateTime creationTime = File.GetCreationTimeUtc(Path.Combine(StorageFolder, file)); + // if the file is older then 5 minutes - delete it. + if (DateTime.UtcNow - creationTime >= TimeSpan.FromMinutes(5)) + { + File.Delete(Path.Combine(StorageFolder, file)); + } + } + } + catch (Exception e) + { + PersistenceChannelDebugLog.WriteException(e, "Failed to delete tmp files."); + } + } +} diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs new file mode 100644 index 000000000000..69b6132f1b9f --- /dev/null +++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.Globalization; +using Microsoft.ApplicationInsights.Channel; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel; + +internal class StorageTransmission : Transmission, IDisposable +{ + internal Action Disposing; + + protected StorageTransmission(string fullPath, Uri address, byte[] content, string contentType, + string contentEncoding) + : base(address, content, contentType, contentEncoding) + { + FullFilePath = fullPath; + FileName = Path.GetFileName(fullPath); + } + + internal string FileName { get; } + + internal string FullFilePath { get; } + + /// + /// Disposing the storage transmission. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Creates a new transmission from the specified . + /// + /// Return transmission loaded from file; return null if the file is corrupted. + internal static async Task CreateFromStreamAsync(Stream stream, string fileName) + { + StreamReader reader = new(stream); + Uri address = await ReadAddressAsync(reader).ConfigureAwait(false); + string contentType = await ReadHeaderAsync(reader, "Content-Type").ConfigureAwait(false); + string contentEncoding = await ReadHeaderAsync(reader, "Content-Encoding").ConfigureAwait(false); + byte[] content = await ReadContentAsync(reader).ConfigureAwait(false); + return new StorageTransmission(fileName, address, content, contentType, contentEncoding); + } + + /// + /// Saves the transmission to the specified . + /// + internal static async Task SaveAsync(Transmission transmission, Stream stream) + { + StreamWriter writer = new(stream); + try + { + await writer.WriteLineAsync(transmission.EndpointAddress.ToString()).ConfigureAwait(false); + await writer.WriteLineAsync("Content-Type" + ":" + transmission.ContentType).ConfigureAwait(false); + await writer.WriteLineAsync("Content-Encoding" + ":" + transmission.ContentEncoding) + .ConfigureAwait(false); + await writer.WriteLineAsync(string.Empty).ConfigureAwait(false); + await writer.WriteAsync(Convert.ToBase64String(transmission.Content)).ConfigureAwait(false); + } + finally + { + writer.Flush(); + } + } + + private static async Task ReadHeaderAsync(TextReader reader, string headerName) + { + string line = await reader.ReadLineAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(line)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "{0} header is expected.", + headerName)); + } + + string[] parts = line.Split(':'); + if (parts.Length != 2) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "Unexpected header format. {0} header is expected. Actual header: {1}", headerName, line)); + } + + if (parts[0] != headerName) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "{0} header is expected. Actual header: {1}", headerName, line)); + } + + return parts[1].Trim(); + } + + private static async Task ReadAddressAsync(TextReader reader) + { + string addressLine = await reader.ReadLineAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(addressLine)) + { + throw new FormatException("Transmission address is expected."); + } + + Uri address = new(addressLine); + return address; + } + + private static async Task ReadContentAsync(TextReader reader) + { + string content = await reader.ReadToEndAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(content) || content == Environment.NewLine) + { + throw new FormatException("Content is expected."); + } + + return Convert.FromBase64String(content); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + Action disposingDelegate = Disposing; + disposingDelegate?.Invoke(this); + } + } +} diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs new file mode 100644 index 000000000000..38f0d1c7ca19 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -0,0 +1,263 @@ +// 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.Diagnostics; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Configurer; +using CLIRuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; + +namespace Microsoft.DotNet.Cli.Telemetry; + +public class Telemetry : ITelemetry +{ + internal static string? CurrentSessionId = null; + internal static bool DisabledForTests = false; + private readonly int _senderCount; + private TelemetryClient? _client = null; + private FrozenDictionary? _commonProperties = null; + private FrozenDictionary? _commonMeasurements = null; + private Task? _trackEventTask = null; + + private const string ConnectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254"; + + public bool Enabled { get; } + + public Telemetry() : this(null) { } + + public Telemetry(IFirstTimeUseNoticeSentinel? sentinel) : this(sentinel, null) { } + + public Telemetry( + IFirstTimeUseNoticeSentinel? sentinel, + string? sessionId, + bool blockThreadInitialization = false, + IEnvironmentProvider? environmentProvider = null, + int senderCount = 3) + { + + if (DisabledForTests) + { + return; + } + + environmentProvider ??= new EnvironmentProvider(); + + Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault) + && PermissionExists(sentinel); + + if (!Enabled) + { + return; + } + + // Store the session ID in a static field so that it can be reused + CurrentSessionId = sessionId ?? Guid.NewGuid().ToString(); + _senderCount = senderCount; + if (blockThreadInitialization) + { + InitializeTelemetry(); + } + else + { + //initialize in task to offload to parallel thread + _trackEventTask = Task.Run(() => InitializeTelemetry()); + } + } + + internal static void DisableForTests() + { + DisabledForTests = true; + CurrentSessionId = null; + } + + internal static void EnableForTests() + { + DisabledForTests = false; + } + + private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel) + { + if (sentinel == null) + { + return false; + } + + return sentinel.Exists(); + } + + public void TrackEvent(string eventName, IDictionary properties, + IDictionary measurements) + { + if (!Enabled) + { + return; + } + + //continue the task in different threads + if (_trackEventTask == null) + { + _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements)); + return; + } + else + { + _trackEventTask = _trackEventTask.ContinueWith( + x => TrackEventTask(eventName, properties, measurements) + ); + } + } + + public void Flush() + { + if (!Enabled || _trackEventTask == null) + { + return; + } + + _trackEventTask.Wait(); + } + + // Adding dispose on graceful shutdown per https://github.com/microsoft/ApplicationInsights-dotnet/issues/1152#issuecomment-518742922 + public void Dispose() + { + if (_client != null) + { + _client.TelemetryConfiguration.Dispose(); + _client = null; + } + } + + public void ThreadBlockingTrackEvent(string eventName, IDictionary properties, IDictionary measurements) + { + if (!Enabled) + { + return; + } + TrackEventTask(eventName, properties, measurements); + } + + private void InitializeTelemetry() + { + try + { + var persistenceChannel = new PersistenceChannel.PersistenceChannel(sendersCount: _senderCount) + { + SendingInterval = TimeSpan.FromMilliseconds(1) + }; + + var config = TelemetryConfiguration.CreateDefault(); + config.TelemetryChannel = persistenceChannel; + config.ConnectionString = ConnectionString; + _client = new TelemetryClient(config); + _client.Context.Session.Id = CurrentSessionId; + _client.Context.Device.OperatingSystem = CLIRuntimeEnvironment.OperatingSystem; + + _commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(CurrentSessionId); + _commonMeasurements = FrozenDictionary.Empty; + } + catch (Exception e) + { + _client = null; + // we dont want to fail the tool if telemetry fails. + Debug.Fail(e.ToString()); + } + } + + private void TrackEventTask( + string eventName, + IDictionary properties, + IDictionary measurements) + { + if (_client == null) + { + return; + } + + try + { + var eventProperties = GetEventProperties(properties); + var eventMeasurements = GetEventMeasures(measurements); + + eventProperties ??= new Dictionary(); + eventProperties.Add("event id", Guid.NewGuid().ToString()); + + _client.TrackEvent(PrependProducerNamespace(eventName), eventProperties, eventMeasurements); + Activity.Current?.AddEvent(CreateActivityEvent(eventName, eventProperties, eventMeasurements)); + } + catch (Exception e) + { + Debug.Fail(e.ToString()); + } + } + + private static ActivityEvent CreateActivityEvent( + string eventName, + IDictionary? properties, + IDictionary? measurements) + { + var tags = MakeTags(properties, measurements); + return new ActivityEvent( + PrependProducerNamespace(eventName), + tags: tags); + } + + private static ActivityTagsCollection? MakeTags( + IDictionary? properties, + IDictionary? measurements) + { + if (properties == null && measurements == null) + { + return null; + } + else if (properties != null && measurements == null) + { + return [.. properties.Select(p => new KeyValuePair(p.Key, p.Value))]; + } + else if (properties == null && measurements != null) + { + return [.. measurements.Select(m => new KeyValuePair(m.Key, m.Value.ToString()))]; + } + else return [ .. properties!.Select(p => new KeyValuePair(p.Key, p.Value)), + .. measurements!.Select(m => new KeyValuePair(m.Key, m.Value.ToString())) ]; + } + + private static string PrependProducerNamespace(string eventName) => $"dotnet/cli/{eventName}"; + + private IDictionary? GetEventMeasures(IDictionary? measurements) + { + return (measurements, _commonMeasurements) switch + { + (null, null) => null, + (null, not null) => _commonMeasurements == FrozenDictionary.Empty ? null : new Dictionary(_commonMeasurements), + (not null, null) => measurements, + (not null, not null) => Combine(_commonMeasurements, measurements), + }; + } + + private IDictionary? GetEventProperties(IDictionary? properties) + { + return (properties, _commonProperties) switch + { + (null, null) => null, + (null, not null) => _commonProperties == FrozenDictionary.Empty ? null : new Dictionary(_commonProperties), + (not null, null) => properties, + (not null, not null) => Combine(_commonProperties, properties), + }; + } + + static IDictionary Combine(IDictionary common, IDictionary specific) where TKey : notnull + { + IDictionary eventMeasurements = new Dictionary(capacity: common.Count + specific.Count); + foreach (KeyValuePair measurement in common) + { + eventMeasurements[measurement.Key] = measurement.Value; + } + foreach (KeyValuePair measurement in specific) + { + eventMeasurements[measurement.Key] = measurement.Value; + } + return eventMeasurements; + } +} diff --git a/src/Cli/dotnet/Telemetry/TelemetryClient.cs b/src/Cli/dotnet/Telemetry/TelemetryClient.cs deleted file mode 100644 index aaba4e686ef1..000000000000 --- a/src/Cli/dotnet/Telemetry/TelemetryClient.cs +++ /dev/null @@ -1,231 +0,0 @@ -// 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.Diagnostics; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Configurer; - -#if TARGET_WINDOWS -using Azure.Monitor.OpenTelemetry.Exporter; -using OpenTelemetry; -using OpenTelemetry.Context.Propagation; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -#endif - -namespace Microsoft.DotNet.Cli.Telemetry; - -public class TelemetryClient : ITelemetryClient -{ - private static FrozenDictionary s_commonProperties = []; - private Task? _trackEventTask; - -#if TARGET_WINDOWS - private static readonly MeterProviderBuilder s_metricsProviderBuilder; - private static MeterProvider? s_metricsProvider; - private static readonly TracerProviderBuilder s_tracerProviderBuilder; - private static TracerProvider? s_tracerProvider; - private static readonly List s_activities = []; - - private static readonly string s_connectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254"; - private static readonly string s_defaultStorageDirectory = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, "TelemetryStorageService"); - // Note: The TelemetryClient instance constructor takes in an environment provider. These fields don't use that currently. - private static readonly string? s_environmentStoragePath = Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_STORAGE_PATH); - private static readonly string? s_diskLogPath = Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_LOG_PATH); - private static readonly bool s_disableTraceExport = Env.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT); - private static readonly int s_flushTimeoutMs = 200; -#endif - - public static string? CurrentSessionId { get; private set; } = null; - public static bool DisabledForTests - { - get => field; - set - { - field = value; - // When disabled, clear the session ID. - if (field) - { - CurrentSessionId = null; - } - } - } = false; - public static ActivityContext ParentActivityContext { get; private set; } - public static ActivityKind ActivityKind { get; private set; } - - public bool Enabled { get; } - - static TelemetryClient() - { -#if TARGET_WINDOWS - s_metricsProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(r => { r.AddService("dotnet-cli", serviceVersion: Product.Version); }) - .AddMeter(Activities.Source.Name) - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddOtlpExporter(); - - s_tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureResource(r => { r.AddService("dotnet-cli", serviceVersion: Product.Version); }) - .AddSource(Activities.Source.Name) - .AddHttpClientInstrumentation() - .AddOtlpExporter() - .AddInMemoryExporter(s_activities) - .SetSampler(new AlwaysOnSampler()); - - if (!s_disableTraceExport) - { - var storageDirectory = string.IsNullOrWhiteSpace(s_environmentStoragePath) ? s_defaultStorageDirectory : s_environmentStoragePath; - s_tracerProviderBuilder.AddAzureMonitorTraceExporter(o => - { - o.ConnectionString = s_connectionString; - o.EnableLiveMetrics = false; - o.StorageDirectory = storageDirectory; - }); - } -#endif - - var parentActivityContext = GetParentActivityContext(); - ActivityKind = GetActivityKind(parentActivityContext); - ParentActivityContext = parentActivityContext ?? default; - } - - public TelemetryClient() : this(null) { } - - public TelemetryClient(string? sessionId, IEnvironmentProvider? environmentProvider = null) - { - // This is some kind of special condition for MSBuild-related tests. - if (DisabledForTests) - { - return; - } - - environmentProvider ??= new EnvironmentProvider(); - Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, - // When building in the official CI pipeline, this makes the complier enable telemetry by default. Otherwise, it is disabled. - // It is the reason tests don't send telemetry, because we don't run tests in the official CI pipeline. - defaultValue: CompileOptions.TelemetryOptOutDefault); - if (!Enabled) - { - return; - } - -#if TARGET_WINDOWS - if (s_metricsProvider is null || s_tracerProvider is null) - { - // Create a new OTel meter and tracer provider. - // It is important to keep the provider instances active throughout the process lifetime. - s_metricsProvider ??= s_metricsProviderBuilder.Build(); - s_tracerProvider ??= s_tracerProviderBuilder.Build(); - } -#endif - - CurrentSessionId ??= !string.IsNullOrEmpty(sessionId) ? sessionId : Guid.NewGuid().ToString(); - s_commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(CurrentSessionId); - } - - /// - /// Uses the OpenTelemetry SDK's Propagation API to derive the parent activity context from the DOTNET_CLI_TRACEPARENT and DOTNET_CLI_TRACESTATE environment variables. - /// - private static ActivityContext? GetParentActivityContext() - { - var traceParent = Env.GetEnvironmentVariable(Activities.TRACEPARENT); - if (string.IsNullOrEmpty(traceParent)) - { - return null; - } - - var carrierMap = new Dictionary?> { { "traceparent", [traceParent] } }; - var traceState = Env.GetEnvironmentVariable(Activities.TRACESTATE); - if (!string.IsNullOrEmpty(traceState)) - { - carrierMap.Add("tracestate", [traceState]); - } - - ActivityContext? parentContext = null; -#if TARGET_WINDOWS - // Use the propegator to extract the parent activity context and kind. - // For some reason, this isn't set by the OTel SDK like docs say it should be. - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator([new TraceContextPropagator(), new BaggagePropagator()])); - parentContext = Propagators.DefaultTextMapPropagator.Extract(default, carrierMap, GetValueFromCarrier).ActivityContext; -#endif - return parentContext; - -#if TARGET_WINDOWS - static IEnumerable? GetValueFromCarrier(Dictionary?> carrier, string key) => - carrier.TryGetValue(key, out var value) ? value : null; -#endif - } - - private static ActivityKind GetActivityKind(ActivityContext? parentActivityContext) => - parentActivityContext is ActivityContext { IsRemote: true } ? ActivityKind.Server : ActivityKind.Internal; - - public static void FlushProviders() - { -#if TARGET_WINDOWS - s_tracerProvider?.ForceFlush(s_flushTimeoutMs); - s_metricsProvider?.ForceFlush(s_flushTimeoutMs); -#endif - } - - public static void WriteLogIfNecessary() - { -#if TARGET_WINDOWS - if (!string.IsNullOrWhiteSpace(s_diskLogPath) && s_activities.Any()) - { - TelemetryDiskLogger.WriteLog(s_diskLogPath, s_activities); - } -#endif - } - - public void TrackEvent(string eventName, IDictionary? properties) - { - if (!Enabled) - { - return; - } - - // Continue the task in different threads. - _trackEventTask = _trackEventTask == null - ? Task.Run(() => TrackEventTask(eventName, properties)) - : _trackEventTask.ContinueWith(_ => TrackEventTask(eventName, properties)); - } - - public void ThreadBlockingTrackEvent(string eventName, IDictionary? properties) - { - if (!Enabled) - { - return; - } - - TrackEventTask(eventName, properties); - } - - private static void TrackEventTask(string eventName, IDictionary? properties) - { - try - { - properties ??= new Dictionary(); - properties.Add("event id", Guid.NewGuid().ToString()); - var @event = new ActivityEvent($"dotnet/cli/{eventName}", tags: MakeTags(properties)); - Activity.Current?.AddEvent(@event); - } - catch (Exception e) - { - Debug.Fail(e.ToString()); - } - } - - private static ActivityTagsCollection MakeTags(IDictionary eventProperties) - { - var common = s_commonProperties - .Select(p => new KeyValuePair(p.Key, p.Value)); - var properties = eventProperties - .Where(p => p.Value is not null) - .Select(p => new KeyValuePair(p.Key, p.Value)) - .OrderBy(p => p.Key); - return [.. common, .. properties]; - } -} diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index 12d8395b7688..8c71bff14e7d 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.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. +#nullable disable + using System.Collections.Frozen; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; @@ -11,24 +13,23 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal class TelemetryCommonProperties( - Func? getCurrentDirectory = null, - Func? hasher = null, - Func? getMACAddress = null, - Func? getDeviceId = null, - IDockerContainerDetector? dockerContainerDetector = null, - IUserLevelCacheWriter? userLevelCacheWriter = null, - ICIEnvironmentDetector? ciEnvironmentDetector = null, - ILLMEnvironmentDetector? llmEnvironmentDetector = null) + Func getCurrentDirectory = null, + Func hasher = null, + Func getMACAddress = null, + Func getDeviceId = null, + IDockerContainerDetector dockerContainerDetector = null, + IUserLevelCacheWriter userLevelCacheWriter = null, + ICIEnvironmentDetector ciEnvironmentDetector = null, + ILLMEnvironmentDetector llmEnvironmentDetector = null) { private readonly IDockerContainerDetector _dockerContainerDetector = dockerContainerDetector ?? new DockerContainerDetectorForTelemetry(); private readonly ICIEnvironmentDetector _ciEnvironmentDetector = ciEnvironmentDetector ?? new CIEnvironmentDetectorForTelemetry(); private readonly ILLMEnvironmentDetector _llmEnvironmentDetector = llmEnvironmentDetector ?? new LLMEnvironmentDetectorForTelemetry(); private readonly Func _getCurrentDirectory = getCurrentDirectory ?? Directory.GetCurrentDirectory; private readonly Func _hasher = hasher ?? Sha256Hasher.Hash; - private readonly Func _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress; + private readonly Func _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress; private readonly Func _getDeviceId = getDeviceId ?? DeviceIdGetter.GetDeviceId; private readonly IUserLevelCacheWriter _userLevelCacheWriter = userLevelCacheWriter ?? new UserLevelCacheWriter(); - private const string OSVersion = "OS Version"; private const string OSPlatform = "OS Platform"; private const string OSArchitecture = "OS Architecture"; @@ -47,39 +48,63 @@ internal class TelemetryCommonProperties( private const string LibcRelease = "Libc Release"; private const string LibcVersion = "Libc Version"; private const string SessionId = "SessionId"; + private const string CI = "Continuous Integration"; private const string LLM = "llm"; + private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE"; + private const string CannotFindMacAddress = "Unknown"; + private const string MachineIdCacheKey = "MachineId"; private const string IsDockerContainerCacheKey = "IsDockerContainer"; - public FrozenDictionary GetTelemetryCommonProperties(string? currentSessionId) => new Dictionary + public FrozenDictionary GetTelemetryCommonProperties(string currentSessionId) { - { OSVersion, RuntimeEnvironment.OperatingSystemVersion }, - { OSPlatform, RuntimeEnvironment.OperatingSystemPlatform.ToString() }, - { OSArchitecture, RuntimeInformation.OSArchitecture.ToString() }, - { OutputRedirected, Console.IsOutputRedirected.ToString() }, - { RuntimeId, RuntimeInformation.RuntimeIdentifier }, - { ProductVersion, Product.Version }, - { TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable) }, - { DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") ) }, - { CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() }, - { LLM, _llmEnvironmentDetector.GetLLMEnvironment() }, - { CurrentPathHash, _hasher(_getCurrentDirectory()) }, - { MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId) }, - // We don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions. - // If we change the format of the cache later, we need to rename the cache from v1 to v2. - { MachineId, _userLevelCacheWriter.RunWithCacheInFilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, $"{MachineIdCacheKey}.v1.dotnetUserLevelCache"), GetMachineId) }, - { DeviceId, _getDeviceId() }, - { KernelVersion, GetKernelVersion() }, - { InstallationType, ExternalTelemetryProperties.GetInstallationType() }, - { ProductType, ExternalTelemetryProperties.GetProductType() }, - { LibcRelease, ExternalTelemetryProperties.GetLibcRelease() }, - { LibcVersion, ExternalTelemetryProperties.GetLibcVersion() }, - { SessionId, currentSessionId } - }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + return new Dictionary + { + {OSVersion, RuntimeEnvironment.OperatingSystemVersion}, + {OSPlatform, RuntimeEnvironment.OperatingSystemPlatform.ToString()}, + {OSArchitecture, RuntimeInformation.OSArchitecture.ToString()}, + {OutputRedirected, Console.IsOutputRedirected.ToString()}, + {RuntimeId, RuntimeInformation.RuntimeIdentifier}, + {ProductVersion, Product.Version}, + {TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable)}, + {DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )}, + {CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() }, + {LLM, _llmEnvironmentDetector.GetLLMEnvironment() }, + {CurrentPathHash, _hasher(_getCurrentDirectory())}, + {MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId)}, + // we don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions. + // If we change the format of the cache later. + // We need to rename the cache from v1 to v2 + {MachineId, + _userLevelCacheWriter.RunWithCacheInFilePath( + Path.Combine( + CliFolderPathCalculator.DotnetUserProfileFolderPath, + $"{MachineIdCacheKey}.v1.dotnetUserLevelCache"), + GetMachineId)}, + {DeviceId, _getDeviceId()}, + {KernelVersion, GetKernelVersion()}, + {InstallationType, ExternalTelemetryProperties.GetInstallationType()}, + {ProductType, ExternalTelemetryProperties.GetProductType()}, + {LibcRelease, ExternalTelemetryProperties.GetLibcRelease()}, + {LibcVersion, ExternalTelemetryProperties.GetLibcVersion()}, + {SessionId, currentSessionId} + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } - private string GetMachineId() => _getMACAddress() is { } macAddress ? _hasher(macAddress) : Guid.NewGuid().ToString(); + private string GetMachineId() + { + var macAddress = _getMACAddress(); + if (macAddress != null) + { + return _hasher(macAddress); + } + else + { + return Guid.NewGuid().ToString(); + } + } /// /// Returns a string identifying the OS kernel. @@ -115,5 +140,8 @@ internal class TelemetryCommonProperties( /// Windows.7 Microsoft Windows 6.1.7601 S /// Windows.81 Microsoft Windows 6.3.9600 /// - private static string GetKernelVersion() => RuntimeInformation.OSDescription; + private static string GetKernelVersion() + { + return RuntimeInformation.OSDescription; + } } diff --git a/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs b/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs deleted file mode 100644 index aa287369406c..000000000000 --- a/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Microsoft.DotNet.Cli.Telemetry; - -internal static class TelemetryDiskLogger -{ - private static readonly JsonSerializerOptions s_jsonOptions; - - private static readonly TelemetryDiskLoggerJsonSerializerContext s_jsonContext; - - public record EventModel( - string name, - DateTimeOffset timestamp, - Dictionary tags); - - public record SourceModel( - string name, - string? version, - Dictionary? tags); - - public record IdentifiersModel( - string? id, - string traceId, - string spanId, - string parentSpanId, - string? parentId, - string? rootId); - - public record ActivityModel( - string operationName, - string displayName, - TimeSpan duration, - IdentifiersModel identifiers, - SourceModel source, - Dictionary tags, - EventModel[] events); - - static TelemetryDiskLogger() - { - s_jsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false }; - s_jsonContext = new(s_jsonOptions); - } - - public static void WriteLog(string logPath, IEnumerable activies) - { - try - { - var jsonText = !File.Exists(logPath) ? """{"activities":[]}""" : File.ReadAllText(logPath); - var root = JsonNode.Parse(jsonText)!; - var activitiesArray = root["activities"]!.AsArray(); - activitiesArray.AddRange(activies.Select(r => JsonNode.Parse(JsonSerializer.Serialize(CreateActivityJsonModel(r), s_jsonContext.ActivityModel)))); - root["activities"] = activitiesArray; - File.WriteAllText(logPath, root.ToJsonString(s_jsonOptions)); - } - catch - { - // Swallow any exceptions to avoid interfering with telemetry shutdown. - } - } - - private static ActivityModel CreateActivityJsonModel(Activity activity) => new( - operationName: activity.OperationName, - displayName: activity.DisplayName, - duration: activity.Duration, - identifiers: new( - id: activity.Id, - traceId: activity.TraceId.ToString(), - spanId: activity.SpanId.ToString(), - parentSpanId: activity.ParentSpanId.ToString(), - parentId: activity.ParentId, - rootId: activity.RootId - ), - source: new( - name: activity.Source.Name, - version: activity.Source.Version, - tags: activity.Source.Tags?.ToDictionary() - ), - tags: activity.Tags.ToDictionary(), - events: [.. activity.Events.Select(e => new EventModel( - name: e.Name, - timestamp: e.Timestamp, - tags: e.Tags.ToDictionary() - ))] - ); -} - -[JsonSerializable(typeof(TelemetryDiskLogger.ActivityModel))] -internal partial class TelemetryDiskLoggerJsonSerializerContext : JsonSerializerContext; diff --git a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs index 00a758add1de..36d088115a01 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs @@ -1,97 +1,109 @@ // 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 System.CommandLine; using System.Globalization; using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Commands.Build; +using Microsoft.DotNet.Cli.Commands.Clean; +using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess; +using Microsoft.DotNet.Cli.Commands.Pack; +using Microsoft.DotNet.Cli.Commands.Publish; +using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Commands.Test; using Microsoft.DotNet.Cli.Commands.VSTest; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Telemetry; -internal class TelemetryFilter(Func? hash) : ITelemetryFilter +internal class TelemetryFilter(Func hash) : ITelemetryFilter { private const string ExceptionEventName = "mainCatchException/exception"; private readonly Func _hash = hash ?? throw new ArgumentNullException(nameof(hash)); - public IEnumerable Filter(ParseResult parseResult) => - Hash(FilterImpl(parseResult, globalJsonState: null)); - - public IEnumerable Filter(ParseResultWithGlobalJsonState parseData) => - Hash(FilterImpl(parseData.ParseResult, parseData.GlobalJsonState)); - - public IEnumerable Filter(InstallerSuccessReport report) - { - var reportProperties = new Dictionary - { - { "exeName", report.ExeName } - }; - return Hash([new TelemetryEntryFormat("install/reportsuccess", reportProperties)]); - } - - public IEnumerable Filter(Exception exception) + public IEnumerable Filter(object objectToFilter) { - var exceptionProperties = new Dictionary + var result = new List(); + Dictionary measurements = null; + string globalJsonState = string.Empty; + if (objectToFilter is Tuple> parseResultWithMeasurements) { - { "exceptionType", exception.GetType().ToString() }, - { "detail", ExceptionToStringWithoutMessage(exception) } - }; - return Hash([new TelemetryEntryFormat(ExceptionEventName, exceptionProperties)]); - } - - private static IEnumerable FilterImpl(ParseResult parseResult, string? globalJsonState) - { - var topLevelCommandName = parseResult.RootSubCommandResult(); - if (topLevelCommandName is null) - { - yield break; + objectToFilter = parseResultWithMeasurements.Item1; + measurements = parseResultWithMeasurements.Item2; + measurements = RemoveZeroTimes(measurements); } - - Dictionary properties = new() { ["verb"] = topLevelCommandName }; - if (!string.IsNullOrEmpty(globalJsonState)) + else if (objectToFilter is Tuple, string> parseResultWithMeasurementsAndGlobalJsonState) { - properties["globalJson"] = globalJsonState; + objectToFilter = parseResultWithMeasurementsAndGlobalJsonState.Item1; + measurements = parseResultWithMeasurementsAndGlobalJsonState.Item2; + measurements = RemoveZeroTimes(measurements); + globalJsonState = parseResultWithMeasurementsAndGlobalJsonState.Item3; } - yield return new TelemetryEntryFormat("toplevelparser/command", properties); - - if (parseResult.IsDotnetBuiltInCommand() && - parseResult.SafelyGetValueForOption("--verbosity") is VerbosityOptions verbosity) + if (objectToFilter is ParseResult parseResult) { - var verbosityProperties = new Dictionary() + var topLevelCommandName = parseResult.RootSubCommandResult(); + if (topLevelCommandName != null) { - { "verb", topLevelCommandName}, - { "verbosity", Enum.GetName(verbosity)} - }; - yield return new TelemetryEntryFormat("sublevelparser/command", verbosityProperties); + Dictionary properties = new() + { + ["verb"] = topLevelCommandName + }; + if (!string.IsNullOrEmpty(globalJsonState)) + { + properties["globalJson"] = globalJsonState; + } + + result.Add(new ApplicationInsightsEntryFormat( + "toplevelparser/command", + properties, + measurements + )); + + LogVerbosityForAllTopLevelCommand(result, parseResult, topLevelCommandName, measurements); + LogVulnerableOptionForPackageUpdateCommand(result, parseResult, topLevelCommandName, measurements); + + foreach (IParseResultLogRule rule in ParseResultLogRules) + { + result.AddRange(rule.AllowList(parseResult, measurements)); + } + } } - - if (topLevelCommandName == "package" && - parseResult.CommandResult.Command != null && - parseResult.CommandResult.Command.Name == "update") + else if (objectToFilter is InstallerSuccessReport installerSuccessReport) { - var hasVulnerableOption = parseResult.HasOption("--vulnerable"); - var vulnerableProperties = new Dictionary() - { - { "verb", "package update" }, - { "vulnerable", hasVulnerableOption.ToString()} - }; - yield return new TelemetryEntryFormat("sublevelparser/command", vulnerableProperties); + result.Add(new ApplicationInsightsEntryFormat( + "install/reportsuccess", + new Dictionary { { "exeName", installerSuccessReport.ExeName } } + )); + } + else if (objectToFilter is Exception exception) + { + result.Add(new ApplicationInsightsEntryFormat( + ExceptionEventName, + new Dictionary + { + {"exceptionType", exception.GetType().ToString()}, + {"detail", ExceptionToStringWithoutMessage(exception) } + } + )); } - foreach (IParseResultLogRule rule in ParseResultLogRules) + return [.. result.Select(r => { - foreach (TelemetryEntryFormat allowList in rule.AllowList(parseResult)) + if (r.EventName == ExceptionEventName) { - yield return allowList; + return r; } - } + else + { + return r.WithAppliedToPropertiesValue(_hash); + } + })]; } - public IEnumerable Hash(IEnumerable entries) => - entries.Select(entry => entry.EventName == ExceptionEventName ? entry : entry.WithAppliedToPropertiesValue(_hash)); - private static List ParseResultLogRules => [ new AllowListToSendFirstArgument(["new", "help"]), @@ -124,6 +136,47 @@ public IEnumerable Hash(IEnumerable new AllowListToSendVerbSecondVerbFirstArgument(["workload", "tool", "new"]), ]; + private static void LogVulnerableOptionForPackageUpdateCommand( + ICollection result, + ParseResult parseResult, + string topLevelCommandName, + Dictionary measurements = null) + { + if (topLevelCommandName == "package" && parseResult.CommandResult.Command != null && parseResult.CommandResult.Command.Name == "update") + { + var hasVulnerableOption = parseResult.HasOption("--vulnerable"); + + result.Add(new ApplicationInsightsEntryFormat( + "sublevelparser/command", + new Dictionary() + { + { "verb", "package update" }, + { "vulnerable", hasVulnerableOption.ToString()} + }, + measurements)); + } + } + + private static void LogVerbosityForAllTopLevelCommand( + ICollection result, + ParseResult parseResult, + string topLevelCommandName, + Dictionary measurements = null) + { + if (parseResult.IsDotnetBuiltInCommand() && + parseResult.SafelyGetValueForOption("--verbosity") is VerbosityOptions verbosity) + { + result.Add(new ApplicationInsightsEntryFormat( + "sublevelparser/command", + new Dictionary() + { + { "verb", topLevelCommandName}, + { "verbosity", Enum.GetName(verbosity)} + }, + measurements)); + } + } + private static string ExceptionToStringWithoutMessage(Exception e) { const string AggregateException_ToString = "{0}{1}---> (Inner Exception #{2}) {3}{4}{5}"; @@ -156,18 +209,42 @@ private static string NonAggregateExceptionToStringWithoutMessage(Exception e) string s; const string Exception_EndOfInnerExceptionStack = "--- End of inner exception stack trace ---"; + s = e.GetType().ToString(); + if (e.InnerException != null) { s = s + " ---> " + ExceptionToStringWithoutMessage(e.InnerException) + Environment.NewLine + " " + Exception_EndOfInnerExceptionStack; + } var stackTrace = e.StackTrace; + if (stackTrace != null) { s += Environment.NewLine + stackTrace; } + return s; } + + private static Dictionary RemoveZeroTimes(Dictionary measurements) + { + if (measurements != null) + { + foreach (var measurement in measurements) + { + if (measurement.Value == 0) + { + measurements.Remove(measurement.Key); + } + } + if (measurements.Count == 0) + { + measurements = null; + } + } + return measurements; + } } diff --git a/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs b/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs index a19e98c315ba..62d9d6e93630 100644 --- a/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs +++ b/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.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. +#nullable disable + using System.CommandLine; using System.CommandLine.Parsing; using Microsoft.DotNet.Cli.Extensions; @@ -9,15 +11,17 @@ namespace Microsoft.DotNet.Cli.Telemetry; -internal class TopLevelCommandNameAndOptionToLog(HashSet topLevelCommandName, HashSet optionsToLog) : IParseResultLogRule +internal class TopLevelCommandNameAndOptionToLog( + HashSet topLevelCommandName, + HashSet optionsToLog) : IParseResultLogRule { private HashSet _topLevelCommandName { get; } = topLevelCommandName; private HashSet _optionsToLog { get; } = optionsToLog; - public List AllowList(ParseResult parseResult) + public List AllowList(ParseResult parseResult, Dictionary measurements = null) { var topLevelCommandName = parseResult.RootSubCommandResult(); - var result = new List(); + var result = new List(); foreach (var optionName in _optionsToLog) { if (_topLevelCommandName.Contains(topLevelCommandName) @@ -26,13 +30,14 @@ public List AllowList(ParseResult parseResult) && optionResult.GetValueOrDefault() is object optionValue && optionValue is not null) { - result.Add(new TelemetryEntryFormat( + result.Add(new ApplicationInsightsEntryFormat( "sublevelparser/command", - new Dictionary + new Dictionary { { "verb", topLevelCommandName}, { optionName.RemovePrefix(), Stringify(optionValue) } - })); + }, + measurements)); } } return result; @@ -41,7 +46,7 @@ public List AllowList(ParseResult parseResult) /// /// We're dealing with untyped payloads here, so we need to handle arrays vs non-array values /// - private static string? Stringify(object value) + private static string Stringify(object value) { if (value is null) { diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 8dacbda0359c..de6c67859456 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -6,7 +6,7 @@ Exe MicrosoftAspNetCore true - true + true dotnet5.4 Microsoft.DotNet.Cli $(DefineConstants);EXCLUDE_ASPNETCORE @@ -84,6 +84,7 @@ + @@ -99,15 +100,7 @@ - - - - - - - - - + diff --git a/src/Common/CompileOptions.cs b/src/Common/CompileOptions.cs index cb13ce8aa671..88e90823d6bf 100644 --- a/src/Common/CompileOptions.cs +++ b/src/Common/CompileOptions.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + #if MICROSOFT_ENABLE_TELEMETRY -[assembly: System.Reflection.AssemblyMetadata("TelemetryOptOutDefault", Microsoft.DotNet.Cli.CompileOptions.TelemetryOptOutDefaultString)] + [assembly: System.Reflection.AssemblyMetadata("TelemetryOptOutDefault", Microsoft.DotNet.Cli.CompileOptions.TelemetryOptOutDefaultString)] #endif namespace Microsoft.DotNet.Cli; diff --git a/src/Common/EnvironmentVariableNames.cs b/src/Common/EnvironmentVariableNames.cs index a2dce0704346..6d73b33ab38f 100644 --- a/src/Common/EnvironmentVariableNames.cs +++ b/src/Common/EnvironmentVariableNames.cs @@ -3,7 +3,7 @@ namespace Microsoft.DotNet.Cli; -internal static class EnvironmentVariableNames +static class EnvironmentVariableNames { public static readonly string ALLOW_TARGETING_PACK_CACHING = "DOTNETSDK_ALLOW_TARGETING_PACK_CACHING"; public static readonly string WORKLOAD_PACK_ROOTS = "DOTNETSDK_WORKLOAD_PACK_ROOTS"; @@ -13,24 +13,18 @@ internal static class EnvironmentVariableNames public static readonly string WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS = "DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS"; public static readonly string WORKLOAD_DISABLE_PACK_GROUPS = "DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS"; public static readonly string DISABLE_PUBLISH_AND_PACK_RELEASE = "DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE"; - public static readonly string DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS = nameof(DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS); + public static readonly string DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS = "DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS"; public static readonly string DOTNET_CLI_FORCE_UTF8_ENCODING = nameof(DOTNET_CLI_FORCE_UTF8_ENCODING); public static readonly string TELEMETRY_OPTOUT = "DOTNET_CLI_TELEMETRY_OPTOUT"; - public static readonly string DOTNET_ROOT = nameof(DOTNET_ROOT); - public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG = nameof(DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG); - public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR = nameof(DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR); - public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER = nameof(DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER); - public static readonly string DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT = nameof(DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT); + public static readonly string DOTNET_ROOT = "DOTNET_ROOT"; + public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG = "DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG"; + public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR = "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"; + public static readonly string DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER = "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"; + public static readonly string DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT = "DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"; public static readonly string DOTNET_GENERATE_ASPNET_CERTIFICATE = nameof(DOTNET_GENERATE_ASPNET_CERTIFICATE); public static readonly string DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = nameof(DOTNET_ADD_GLOBAL_TOOLS_TO_PATH); public static readonly string DOTNET_NOLOGO = nameof(DOTNET_NOLOGO); public static readonly string DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK = nameof(DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK); - public static readonly string DOTNET_CLI_TELEMETRY_SESSIONID = nameof(DOTNET_CLI_TELEMETRY_SESSIONID); - public static readonly string DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING = nameof(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING); - // Telemetry logging/debug/testing. - public static readonly string DOTNET_CLI_TELEMETRY_STORAGE_PATH = nameof(DOTNET_CLI_TELEMETRY_STORAGE_PATH); - public static readonly string DOTNET_CLI_TELEMETRY_LOG_PATH = nameof(DOTNET_CLI_TELEMETRY_LOG_PATH); - public static readonly string DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT = nameof(DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT); #if NET7_0_OR_GREATER private static readonly Version s_version6_0 = new(6, 0); diff --git a/src/Microsoft.DotNet.TemplateLocator/TemplateLocator.cs b/src/Microsoft.DotNet.TemplateLocator/TemplateLocator.cs index 653b5608bafd..7ff2785a7b79 100644 --- a/src/Microsoft.DotNet.TemplateLocator/TemplateLocator.cs +++ b/src/Microsoft.DotNet.TemplateLocator/TemplateLocator.cs @@ -36,8 +36,8 @@ public TemplateLocator(Func getEnvironmentVariable, Func GetDotnetSdkTemplatePackages( - string? sdkVersion, - string? dotnetRootPath, + string sdkVersion, + string dotnetRootPath, string? userProfileDir) { if (string.IsNullOrWhiteSpace(sdkVersion)) @@ -47,7 +47,8 @@ public IReadOnlyCollection GetDotnetSdkTemplate if (string.IsNullOrWhiteSpace(dotnetRootPath)) { - throw new ArgumentException($"'{nameof(dotnetRootPath)}' cannot be null or whitespace", nameof(dotnetRootPath)); + throw new ArgumentException($"'{nameof(dotnetRootPath)}' cannot be null or whitespace", + nameof(dotnetRootPath)); } // Will the current directory correspond to the folder we are creating a project in? If we need diff --git a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs index 012d4bb1d797..9bc513f6ff6c 100644 --- a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs +++ b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs @@ -9,11 +9,16 @@ namespace Microsoft.NET.TestFramework.Commands public class SdkCommandSpec { public string? FileName { get; set; } - public List Arguments { get; set; } = []; - public Dictionary Environment { get; set; } = []; - public List EnvironmentToRemove { get; } = []; + public List Arguments { get; set; } = new List(); + + public Dictionary Environment { get; set; } = new(); + + public List EnvironmentToRemove { get; } = new List(); + public string? WorkingDirectory { get; set; } + public bool RedirectStandardInput { get; set; } + public bool DisableOutputAndErrorRedirection { get; set; } private string EscapeArgs() diff --git a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs index f0187fed7b8b..56cf1002d2ae 100644 --- a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs +++ b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs @@ -9,13 +9,19 @@ namespace Microsoft.NET.TestFramework.Commands { public abstract class TestCommand { - private readonly Dictionary _environment = []; + private Dictionary _environment = new(); private bool _doNotEscapeArguments; + public ITestOutputHelper Log { get; } + public string? WorkingDirectory { get; set; } - public List Arguments { get; set; } = []; - public List EnvironmentToRemove { get; } = []; + + public List Arguments { get; set; } = new List(); + + public List EnvironmentToRemove { get; } = new List(); + public bool RedirectStandardInput { get; set; } + public bool DisableOutputAndErrorRedirection { get; set; } // These only work via Execute(), not when using GetProcessStartInfo() diff --git a/test/Microsoft.NET.TestFramework/SdkTest.cs b/test/Microsoft.NET.TestFramework/SdkTest.cs index b377b9b2be92..56c6649e4ae8 100644 --- a/test/Microsoft.NET.TestFramework/SdkTest.cs +++ b/test/Microsoft.NET.TestFramework/SdkTest.cs @@ -3,42 +3,43 @@ using System.Runtime.CompilerServices; -namespace Microsoft.NET.TestFramework; - -public abstract class SdkTest +namespace Microsoft.NET.TestFramework { - protected bool? UsingFullFrameworkMSBuild => SdkTestContext.Current.ToolsetUnderTest?.ShouldUseFullFrameworkMSBuild; + public abstract class SdkTest + { + protected bool? UsingFullFrameworkMSBuild => SdkTestContext.Current.ToolsetUnderTest?.ShouldUseFullFrameworkMSBuild; - protected ITestOutputHelper Log { get; } + protected ITestOutputHelper Log { get; } - protected TestAssetsManager TestAssetsManager { get; } + protected TestAssetsManager TestAssetsManager { get; } - protected SdkTest(ITestOutputHelper log) - { - Log = log; - TestAssetsManager = new TestAssetsManager(log); - } - - protected static void WaitForUtcNowToAdvance() - { - var start = DateTime.UtcNow; + protected SdkTest(ITestOutputHelper log) + { + Log = log; + TestAssetsManager = new TestAssetsManager(log); + } - while (DateTime.UtcNow <= start) + protected static void WaitForUtcNowToAdvance() { - Thread.Sleep(millisecondsTimeout: 1); + var start = DateTime.UtcNow; + + while (DateTime.UtcNow <= start) + { + Thread.Sleep(millisecondsTimeout: 1); + } } - } - /// - /// Generates a MSBuild binlog argument with a unique name based on the caller and provided parts, and places it in a location that will be collected by Helix if running in that environment. - /// - protected string BinLogArgument(ReadOnlySpan parts, [CallerMemberName] string callerName = "") - { - // combine the name and parts into a unique binlog - var fileName = $"{callerName}{(parts.Length > 0 ? "-" + string.Join("-", parts.ToArray()) : "")}-{{}}.binlog"; - var binlogDestPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") is { } ciOutputRoot && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ID") is { } helixGuid ? - Path.Combine(ciOutputRoot, "binlog", helixGuid, fileName) : - $"./{fileName}"; - return $"/bl:{binlogDestPath}"; + /// + /// Generates a MSBuild binlog argument with a unique name based on the caller and provided parts, and places it in a location that will be collected by Helix if running in that environment. + /// + protected string BinLogArgument(ReadOnlySpan parts, [CallerMemberName] string callerName = "") + { + // combine the name and parts into a unique binlog + var fileName = $"{callerName}{(parts.Length > 0 ? "-" + string.Join("-", parts.ToArray()) : "")}-{{}}.binlog"; + var binlogDestPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") is { } ciOutputRoot && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ID") is { } helixGuid ? + Path.Combine(ciOutputRoot, "binlog", helixGuid, fileName) : + $"./{fileName}"; + return $"/bl:{binlogDestPath}"; + } } } diff --git a/test/dotnet.Tests/CliSchemaTests.cs b/test/dotnet.Tests/CliSchemaTests.cs index 71a507d3ebb9..8478d3c0d706 100644 --- a/test/dotnet.Tests/CliSchemaTests.cs +++ b/test/dotnet.Tests/CliSchemaTests.cs @@ -1208,7 +1208,7 @@ public void PrintCliSchema_WritesExpectedJson(string[] commandArgs, string json) { var stream = new MemoryStream(); var writer = new StreamWriter(stream); - CliSchema.PrintCliSchema(Parser.Parse(commandArgs), writer, null); + CliSchema.PrintCliSchema(Parser.Parse(commandArgs).CommandResult, writer, null); stream.Position = 0; var reader = new StreamReader(stream); var output = reader.ReadToEnd(); diff --git a/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs b/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs index d526082c400d..b0ff2d4f0bd4 100644 --- a/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs +++ b/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs @@ -214,23 +214,15 @@ [new RestoredCommandIdentifier( _localToolsResolverCache, _fileSystem); - var commandSpecA = localToolsCommandResolver.Resolve(new CommandResolverArguments() + localToolsCommandResolver.Resolve(new CommandResolverArguments() { CommandName = "dotnet-a", - }); - commandSpecA.Should().NotBeNull(); - var argsA = commandSpecA.Args; - argsA.Should().NotBeNull(); - argsA.Trim('"').Should().Be(fakeExecutableA.Value); + }).Args!.Trim('"').Should().Be(fakeExecutableA.Value); - var commandSpecDotnetA = localToolsCommandResolver.Resolve(new CommandResolverArguments() + localToolsCommandResolver.Resolve(new CommandResolverArguments() { CommandName = "dotnet-dotnet-a", - }); - commandSpecDotnetA.Should().NotBeNull(); - var argsDotnetA = commandSpecDotnetA.Args; - argsDotnetA.Should().NotBeNull(); - argsDotnetA.Trim('"').Should().Be(fakeExecutableDotnetA.Value); + }).Args!.Trim('"').Should().Be(fakeExecutableDotnetA.Value); } private string _jsonContent = diff --git a/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs b/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs index 9f82d05313e5..360342b83c30 100644 --- a/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs +++ b/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs @@ -22,6 +22,7 @@ public static void PerformActionWithBasePath(string basePath, Action action) } CommandDirectoryContext.CurrentBaseDirectory_TestOnly = basePath; + Telemetry.Telemetry.CurrentSessionId = null; try { action(); diff --git a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs index 433b5b5cf2de..a2a26316e3d8 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs @@ -5,7 +5,6 @@ using System.Reflection; using Microsoft.DotNet.Cli.Commands.MSBuild; -using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Configurer; namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -20,7 +19,8 @@ public DotnetMsbuildInProcTests(ITestOutputHelper log) : base(log) [Fact] public void WhenTelemetryIsEnabledTheLoggerIsAddedToTheCommandLine() { - string[] allArgs = GetArgsForMSBuild(() => true, out TelemetryClient telemetry); + Telemetry.Telemetry telemetry; + string[] allArgs = GetArgsForMSBuild(() => true, out telemetry); // telemetry will still be disabled if environment variable is set if (telemetry.Enabled) { @@ -46,13 +46,15 @@ public void WhenTelemetryIsDisabledTheLoggerIsNotAddedToTheCommandLine() private string[] GetArgsForMSBuild(Func sentinelExists) { - return GetArgsForMSBuild(sentinelExists, out TelemetryClient telemetry); + Telemetry.Telemetry telemetry; + return GetArgsForMSBuild(sentinelExists, out telemetry); } - private string[] GetArgsForMSBuild(Func sentinelExists, out TelemetryClient telemetry) + private string[] GetArgsForMSBuild(Func sentinelExists, out Telemetry.Telemetry telemetry) { - TelemetryClient.DisabledForTests = true; // reset static session id modified by telemetry constructor - telemetry = new TelemetryClient(); + + Telemetry.Telemetry.DisableForTests(); // reset static session id modified by telemetry constructor + telemetry = new Telemetry.Telemetry(new MockFirstTimeUseNoticeSentinel(sentinelExists)); MSBuildForwardingApp msBuildForwardingApp = new(Enumerable.Empty()); diff --git a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs index 5bf3435c507f..bcb6a6dbfde1 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs @@ -1,23 +1,34 @@ // 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.Cli.Telemetry; +#nullable disable -namespace Microsoft.DotNet.Cli.MSBuild.Tests; +using Microsoft.DotNet.Cli.Telemetry; -public class FakeTelemetry : ITelemetryClient +namespace Microsoft.DotNet.Cli.MSBuild.Tests { - public bool Enabled { get; set; } = true; + public class FakeTelemetry : ITelemetry + { + public bool Enabled { get; set; } = true; - private readonly List _logEntries = new List(); + private readonly List _logEntries = new List(); - public void TrackEvent(string eventName, IDictionary? properties) - { - var entry = new LogEntry { EventName = eventName, Properties = properties }; - _logEntries.Add(entry); - } + public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements) + { + var entry = new LogEntry { EventName = eventName, Properties = properties, Measurement = measurements }; + _logEntries.Add(entry); + } + + public void Flush() + { + } - public LogEntry? LogEntry => _logEntries.Count > 0 ? _logEntries[_logEntries.Count - 1] : null; + public void Dispose() + { + } - public IReadOnlyList LogEntries => _logEntries.AsReadOnly(); + 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 812ec22b2c8f..ff6543518696 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs @@ -3,7 +3,7 @@ using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Tests.TelemetryTests; +using Microsoft.DotNet.Tests; using BuildCommand = Microsoft.DotNet.Cli.Commands.Build.BuildCommand; namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -122,7 +122,7 @@ public void MsbuildInvocationIsCorrectForSeparateRestore( } [Theory] - [MemberData(memberName: nameof(TelemetryCommonPropertiesTests.LLMTelemetryTestCases), MemberType = typeof(TelemetryCommonPropertiesTests))] + [MemberData(memberName: nameof(TelemetryCommonPropertiesTests.LLMTelemetryTestCases), MemberType =typeof(TelemetryCommonPropertiesTests))] public void WhenLLMIsDetectedTLLiveUpdateIsDisabled(Dictionary? llmEnvVarsToSet, string? expectedLLMName) { CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () => diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs index beeac7148703..e995a09ae611 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// There are tests which modify static TelemetryClient.CurrentSessionId. They cannot run in parallel. + + +// There are tests which modify static Telemetry.CurrentSessionId and they cannot run in parallel [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -104,5 +106,11 @@ public void WhenDotnetRunHelpIsInvokedAppArgumentsTextIsIncludedInOutput() result.ExitCode.Should().Be(0); result.StdOut.Should().Contain(AppArgumentsText); } + + + + } + + } diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs index db007ec0cdc9..9b4dca228442 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Commands.MSBuild; -using Microsoft.DotNet.Cli.Telemetry; using RestoreCommand = Microsoft.DotNet.Cli.Commands.Restore.RestoreCommand; namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -42,7 +41,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona { CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () => { - TelemetryClient.DisabledForTests = true; + Telemetry.Telemetry.DisableForTests(); expectedAdditionalArgs = expectedAdditionalArgs .Select(arg => arg.Replace("", WorkingDirectory)) diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs index d089f147ff6f..c017646949b0 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.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 Microsoft.DotNet.Cli.Telemetry; using TestCommand = Microsoft.DotNet.Cli.Commands.Test.TestCommand; namespace Microsoft.DotNet.Cli.MSBuild.Tests @@ -27,7 +26,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona { CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () => { - TelemetryClient.DisabledForTests = true; + Telemetry.Telemetry.DisableForTests(); expectedAdditionalArgs = expectedAdditionalArgs .Select(arg => arg.Replace("", WorkingDirectory)) diff --git a/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs b/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs index 54ddd2256b19..b2974fba1006 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs @@ -1,20 +1,19 @@ // 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.Cli.Telemetry; - -namespace Microsoft.DotNet.Cli.MSBuild.Tests; - -public class NullCurrentSessionIdFixture +namespace Microsoft.DotNet.Cli.MSBuild.Tests { - public NullCurrentSessionIdFixture() + public class NullCurrentSessionIdFixture { - // We need to set this to guarantee that the telemetry logging - // information will not be added to the msbuild generated parameters - // when testing the translation between CLI params and msbuild params. - // This is now needed because before we set SKIP FIRST RUN in the CLI - // build scripts, but now we don't and we don't want to rely on scripts - // to make our build/tests work. - TelemetryClient.DisabledForTests = true; + public NullCurrentSessionIdFixture() + { + // We need to set this to guarantee that the telemetry logging + // information will not be added to the msbuild generated parameters + // when testing the translation between CLI params and msbuild params. + // This is now needed because before we set SKIP FIRST RUN in the CLI + // build scripts, but now we don't and we don't want to rely on scripts + // to make our build/tests work. + Telemetry.Telemetry.DisableForTests(); + } } } diff --git a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs index 0bfaa370f9cf..ae6425892777 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs @@ -144,9 +144,9 @@ public void CountAdditionalProperties_CountsPropertyDirectives() public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry() { // Arrange - var events = new List<(string? eventName, IDictionary? properties)>(); + var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties)); + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); TelemetryEventEntry.EntryPosted += handler; @@ -171,6 +171,7 @@ 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"); @@ -179,6 +180,12 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry() 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 { @@ -191,9 +198,9 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry() public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry() { // Arrange - var events = new List<(string? eventName, IDictionary? properties)>(); + var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties)); + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); TelemetryEventEntry.EntryPosted += handler; @@ -215,6 +222,7 @@ 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"); @@ -222,6 +230,12 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry() props["launch_profile_requested"].Should().Be("none"); 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 { @@ -234,9 +248,9 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry() public void TrackRunEvent_WithDefaultLaunchProfile_MarksTelemetryCorrectly() { // Arrange - var events = new List<(string? eventName, IDictionary? properties)>(); + var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>(); - void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties)); + void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements)); TelemetryEventEntry.EntryPosted += handler; diff --git a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs index 756766a8aa5f..f5b87dadeee8 100644 --- a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs +++ b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs @@ -259,5 +259,77 @@ public void It_does_not_add_the_tool_path_to_the_environment_if_addGlobalToolsTo _pathAdderMock.Verify(p => p.AddPackageExecutablePathToUserPath(), Times.Never); } + + [Fact] + public void It_does_add_telemetry_when_all_firsttimeuse_values_run() + { + + _firstTimeUseNoticeSentinelMock.Setup(n => n.Exists()).Returns(false); + + Dictionary measurements = new(); + var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer( + _firstTimeUseNoticeSentinelMock.Object, + _aspNetCertificateSentinelMock.Object, + _aspNetCoreCertificateGeneratorMock.Object, + _toolPathSentinelMock.Object, + new DotnetFirstRunConfiguration + ( + generateAspNetCertificate: true, + telemetryOptout: false, + addGlobalToolsToPath: true, + nologo: false, + skipWorkloadIntegrityCheck: false + ), + _reporterMock.Object, + _pathAdderMock.Object, + measurements); + + DateTime beforeConfigure = DateTime.Now; + dotnetFirstTimeUseConfigurer.Configure(); + double configureTime = (DateTime.Now - beforeConfigure).TotalMilliseconds; + + measurements.Should().HaveCount(3); + measurements.Should().ContainKey("AddPackageExecutablePath Time"); + measurements.Should().ContainKey("FirstTimeUseNotice Time"); + measurements.Should().ContainKey("GenerateAspNetCertificate Time"); + measurements["AddPackageExecutablePath Time"].Should().BeGreaterThan(0); + measurements["FirstTimeUseNotice Time"].Should().BeGreaterThan(0); + measurements["GenerateAspNetCertificate Time"].Should().BeGreaterThan(0); + measurements["AddPackageExecutablePath Time"].Should().BeLessThan(configureTime); + measurements["FirstTimeUseNotice Time"].Should().BeLessThan(configureTime); + measurements["GenerateAspNetCertificate Time"].Should().BeLessThan(configureTime); + } + + [Fact] + public void It_does_add_telemetry_when_no_firsttimeuse_values_run() + { + + _firstTimeUseNoticeSentinelMock.Setup(n => n.Exists()).Returns(true); + + Dictionary measurements = new(); + var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer( + _firstTimeUseNoticeSentinelMock.Object, + _aspNetCertificateSentinelMock.Object, + _aspNetCoreCertificateGeneratorMock.Object, + _toolPathSentinelMock.Object, + new DotnetFirstRunConfiguration + ( + generateAspNetCertificate: false, + telemetryOptout: false, + addGlobalToolsToPath: false, + nologo: false, + skipWorkloadIntegrityCheck: false + ), + _reporterMock.Object, + _pathAdderMock.Object, + measurements); + + dotnetFirstTimeUseConfigurer.Configure(); + + measurements.Should().HaveCount(0); + measurements.Should().NotContainKey("AddPackageExecutablePath Time"); + measurements.Should().NotContainKey("FirstTimeUseNotice Time"); + measurements.Should().NotContainKey("GenerateAspNetCertificate Time"); + } } } diff --git a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs index 89cc75f779b1..d694240d865b 100644 --- a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs +++ b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs @@ -28,7 +28,7 @@ public GivenADotnetFirstTimeUseConfigurerWithStateSetup(ITestOutputHelper output private void ResetObjectState() { - TelemetryClient.DisabledForTests = false; + Telemetry.EnableForTests(); _firstTimeUseNoticeSentinelMock = new MockBasicSentinel(); _aspNetCertificateSentinelMock = new MockBasicSentinel(); _aspNetCoreCertificateGeneratorMock = new Mock(MockBehavior.Strict); @@ -183,6 +183,16 @@ public void Assert(ActionCalledTime expectedActionCalledTime) } } + private static ActionCalledTime GetCalledTime(bool predicate, ActionCalledTime actionCalledTime) + { + if (actionCalledTime != FirstRun && predicate) + { + actionCalledTime = SecondRun; + } + + return actionCalledTime; + } + public enum ActionCalledTime { Never, @@ -190,7 +200,7 @@ public enum ActionCalledTime SecondRun } - private TelemetryClient RunConfigUsingMocks(bool isInstallerRun) + private Telemetry RunConfigUsingMocks(bool isInstallerRun) { // Assume the following objects set up are in sync with production behavior. // subject to future refactoring to de-dup with production code. @@ -242,7 +252,10 @@ private TelemetryClient RunConfigUsingMocks(bool isInstallerRun) configurer.Configure(); - return new TelemetryClient("test", environmentProvider: _environmentProviderObject); + return new Telemetry(firstTimeUseNoticeSentinel, + "test", + environmentProvider: _environmentProviderObject, + senderCount: 0); } private class MockBasicSentinel : IFileSentinel, IFirstTimeUseNoticeSentinel, IAspNetCertificateSentinel diff --git a/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs b/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs new file mode 100644 index 000000000000..4ce548e97621 --- /dev/null +++ b/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs @@ -0,0 +1,47 @@ +// 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 System.Collections.Concurrent; +using Microsoft.DotNet.Cli.Telemetry; + +namespace Microsoft.DotNet.Tests +{ + public class FakeRecordEventNameTelemetry : ITelemetry + { + public bool Enabled { get; set; } + + public string EventName { get; set; } + + public void TrackEvent(string eventName, + IDictionary properties, + IDictionary measurements) + { + LogEntries.Add( + new LogEntry + { + EventName = eventName, + Measurement = measurements, + Properties = properties + }); + } + + public void Flush() + { + } + + public void Dispose() + { + } + + public ConcurrentBag LogEntries { get; set; } = new ConcurrentBag(); + + public class LogEntry + { + public string EventName { get; set; } + public IDictionary Properties { get; set; } + public IDictionary Measurement { get; set; } + } + } +} diff --git a/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs b/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs new file mode 100644 index 000000000000..06a8418a6acb --- /dev/null +++ b/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.DotNet.Tests +{ + public class GivenThatTheUserEnablesThePerfLog : SdkTest + { + public GivenThatTheUserEnablesThePerfLog(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public void WhenPerfLogDisabledDotNetDoesNotWriteToThePerfLog() + { + var dir = TestAssetsManager.CreateTestDirectory(); + + var result = new DotnetCommand(Log, "--help") + .WithEnvironmentVariable("DOTNET_PERFLOG_DIR", dir.Path) + .Execute(); + + result.ExitCode.Should().Be(0); + Assert.Empty(new DirectoryInfo(dir.Path).GetFiles()); + } + + [Fact] + public void WhenPerfLogEnabledDotNetWritesToThePerfLog() + { + var dir = TestAssetsManager.CreateTestDirectory(); + + var result = new DotnetCommand(Log, "--help") + .WithEnvironmentVariable("DOTNET_CLI_PERF_LOG", "1") + .WithEnvironmentVariable("DOTNET_PERFLOG_DIR", dir.Path) + .Execute(); + + result.ExitCode.Should().Be(0); + + DirectoryInfo logDir = new(dir.Path); + FileInfo[] logFiles = logDir.GetFiles(); + Assert.NotEmpty(logFiles); + Assert.All(logFiles, f => Assert.StartsWith("perf-", f.Name)); + Assert.All(logFiles, f => Assert.NotEqual(0, f.Length)); + } + + [Fact] + public void WhenPerfLogEnabledDotNetBuildWritesAPerfLog() + { + using (PerfLogTestEventListener listener = new()) + { + int exitCode = Cli.Program.Main(new string[] { "--help" }); + Assert.Equal(0, exitCode); + Assert.NotEqual(0, listener.EventCount); + } + } + } + + internal sealed class PerfLogTestEventListener : EventListener + { + private const string PerfLogEventSourceName = "Microsoft-Dotnet-CLI-Performance"; + + public int EventCount + { + get; private set; + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.Equals(PerfLogEventSourceName)) + { + EnableEvents(eventSource, EventLevel.Verbose); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + Assert.Equal(PerfLogEventSourceName, eventData.EventSource.Name); + EventCount++; + } + } +} diff --git a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs index f0950fe9b9fd..5e9191d076f0 100644 --- a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs +++ b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs @@ -165,7 +165,8 @@ public void ItDoesNotCreateAFirstUseSentinelFileNorAnAspNetCertificateSentinelFi var command = dotnetFirstTime.Setup(Log, TestAssetsManager); - // Disable telemetry to prevent the creation of the .dotnet folder for machineid and docker cache files. + // Disable telemetry to prevent the creation of the .dotnet folder + // for machineid and docker cache files command = command.WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true"); command.Execute("internal-reportinstallsuccess", "test").Should().Pass(); diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs new file mode 100644 index 000000000000..aa180fe14930 --- /dev/null +++ b/test/dotnet.Tests/TelemetryCommandTest.cs @@ -0,0 +1,416 @@ +// 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.Commands.Hidden.InternalReportInstallSuccess; +using Microsoft.DotNet.Cli.Telemetry; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Utilities; + +namespace Microsoft.DotNet.Tests +{ + [Collection(TestConstants.UsesStaticTelemetryState)] + public class TelemetryCommandTests : SdkTest + { + private readonly FakeRecordEventNameTelemetry _fakeTelemetry; + + public string EventName { get; set; } + + public IDictionary Properties { get; set; } + + public TelemetryCommandTests(ITestOutputHelper log) : base(log) + { + _fakeTelemetry = new FakeRecordEventNameTelemetry(); + TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent); + TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + } + + [Fact] + public void NoTelemetryIfCommandIsInvalid() + { + string[] args = { "publish", "-r" }; + Action a = () => { Cli.Program.ProcessArgs(args); }; + a.Should().NotThrow(); + } + + [Fact] + public void NoTelemetryIfCommandIsInvalid2() + { + string[] args = { "restore", "-v" }; + Action a = () => { Cli.Program.ProcessArgs(args); }; + a.Should().NotThrow(); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetry() + { + string[] args = { "help" }; + Cli.Program.ProcessArgs(args); + + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("HELP")); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData() + { + string[] args = { "help" }; + Cli.Program.ProcessArgs(args, new TimeSpan(12345)); + + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("HELP") && + e.Measurement.ContainsKey("Startup Time") && + e.Measurement["Startup Time"] == 1.2345 && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] > 0); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithoutStartupTime() + { + string[] args = { "help" }; + Cli.Program.ProcessArgs(args); + + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("HELP") && + !e.Measurement.ContainsKey("Startup Time") && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] > 0); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryZeroStartupTime() + { + string[] args = { "help" }; + Cli.Program.ProcessArgs(args, new TimeSpan(0)); + + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("HELP") && + !e.Measurement.ContainsKey("Startup Time") && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] > 0); + } + + [Fact] + public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "console"; + string[] args = { "new", argumentToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW")); + } + + [Fact(Skip = "https://github.com/dotnet/sdk/issues/24190")] + public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetryWithPerformanceData() + { + const string argumentToSend = "console"; + string[] args = { "new", argumentToSend }; + Cli.Program.ProcessArgs(args, new TimeSpan(23456)); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW") && + e.Measurement.ContainsKey("Startup Time") && + e.Measurement["Startup Time"] == 2.3456 && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] > 0); + } + + [Fact] + public void DotnetHelpCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "something"; + string[] args = { "help", argumentToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("HELP")); + } + + [Fact] + public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "package"; + string[] args = { "add", argumentToSend, "aPackageName" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("ADD")); + } + + [Fact] + public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry2() + { + const string argumentToSend = "reference"; + string[] args = { "add", argumentToSend, "aPackageName" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("ADD")); + } + + [Fact] + public void DotnetRemoveCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "package"; + string[] args = { "remove", argumentToSend, "aPackageName" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("REMOVE")); + } + + [Fact] + public void DotnetListCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "reference"; + string[] args = { "list", argumentToSend, "aPackageName" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("LIST")); + } + + [Fact] + public void DotnetSlnCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "list"; + string[] args = { "sln", "aSolution", argumentToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("SOLUTION")); + } + + [Fact] + public void DotnetNugetCommandFirstArgumentShouldBeSentToTelemetry() + { + const string argumentToSend = "push"; + + string[] args = { "nuget", argumentToSend, "path" }; + + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NUGET")); + } + + [Fact(Skip = "https://github.com/dotnet/sdk/issues/47862")] + public void DotnetNewCommandLanguageOpinionShouldBeSentToTelemetry() + { + const string optionKey = "language"; + const string optionValueToSend = "c#"; + string[] args = { "new", "console", "--" + optionKey, optionValueToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey(optionKey) && + e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW")); + } + + [Fact] + public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetry() + { + const string optionKey = "verbosity"; + const string optionValueToSend = "minimal"; + string[] args = { "restore", "--" + optionKey, optionValueToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey(optionKey) && + e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("RESTORE")); + } + + [Fact] + public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetryWithPerformanceData() + { + const string optionKey = "verbosity"; + const string optionValueToSend = "minimal"; + string[] args = { "restore", "--" + optionKey, optionValueToSend }; + Cli.Program.ProcessArgs(args, new TimeSpan(34567)); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey(optionKey) && + e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("RESTORE") && + e.Measurement.ContainsKey("Startup Time") && + e.Measurement["Startup Time"] == 3.4567 && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] > 0); + } + + [Fact] + public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetry() + { + const string optionKey = "configuration"; + const string optionValueToSend = "Debug"; + string[] args = { "build", "--" + optionKey, optionValueToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey(optionKey) && + e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); + } + + [Fact] + public void DotnetPublishCommandRuntimeOpinionsShouldBeSentToTelemetry() + { + const string optionKey = "runtime"; + const string optionValueToSend = $"{ToolsetInfo.LatestWinRuntimeIdentifier}-x64"; + string[] args = { "publish", "--" + optionKey, optionValueToSend }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey(optionKey) && + e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("PUBLISH")); + } + + [Fact] + public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption() + { + string[] args = { "build", "--configuration", "Debug", "--runtime", $"{ToolsetInfo.LatestMacRuntimeIdentifier}-x64" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") && + e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); + + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("runtime") && + e.Properties["runtime"] == Sha256Hasher.Hash($"{ToolsetInfo.LatestMacRuntimeIdentifier.ToUpper()}-X64") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); + } + + [Fact] + public void DotnetRunCleanTestCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption() + { + string[] args = { "clean", "--configuration", "Debug", "--framework", ToolsetInfo.CurrentTargetFramework }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") && + e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("CLEAN")); + + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("framework") && + e.Properties["framework"] == Sha256Hasher.Hash(ToolsetInfo.CurrentTargetFramework.ToUpper()) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("CLEAN")); + } + + [Fact] + public void DotnetUpdatePackageVulnerableOptionShouldBeSentToTelemetry() + { + const string optionKey = "vulnerable"; + string[] args = { "package", "update", "--vulnerable" }; + Cli.Program.ProcessArgs(args); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey(optionKey) && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("PACKAGE UPDATE")); + } + + [WindowsOnlyFact] + public void InternalreportinstallsuccessCommandCollectExeNameWithEventname() + { + FakeRecordEventNameTelemetry fakeTelemetry = new(); + string[] args = { "c:\\mypath\\dotnet-sdk-latest-win-x64.exe" }; + + InternalReportInstallSuccessCommand.ProcessInputAndSendTelemetry(args, fakeTelemetry); + + fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "install/reportsuccess" && e.Properties.ContainsKey("exeName") && + e.Properties["exeName"] == Sha256Hasher.Hash("DOTNET-SDK-LATEST-WIN-X64.EXE")); + } + + [Fact] + public void ExceptionShouldBeSentToTelemetry() + { + Exception caughtException = null; + try + { + string[] args = { "build" }; + Cli.Program.ProcessArgs(args); + throw new ArgumentException("test exception"); + } + catch (Exception ex) + { + caughtException = ex; + TelemetryEventEntry.SendFiltered(ex); + } + + var exception = new Exception(); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "mainCatchException/exception" && + e.Properties.ContainsKey("exceptionType") && + e.Properties["exceptionType"] == "System.ArgumentException" && + e.Properties.ContainsKey("detail") && + e.Properties["detail"].Contains(caughtException.StackTrace)); + } + } +} diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs new file mode 100644 index 000000000000..5939d8ffeb2c --- /dev/null +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -0,0 +1,313 @@ +// 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.Cli.Telemetry; +using Microsoft.DotNet.Configurer; + +namespace Microsoft.DotNet.Tests +{ + public class TelemetryCommonPropertiesTests : SdkTest + { + public TelemetryCommonPropertiesTests(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public void TelemetryCommonPropertiesShouldContainIfItIsInDockerOrNot() + { + var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId").Should().ContainKey("Docker Container"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnHashedPath() + { + var unitUnderTest = new TelemetryCommonProperties(() => "ADirectory", userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Current Path Hash"].Should().NotBe("ADirectory"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnHashedMachineId() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"].Should().NotBe("plaintext"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnDevDeviceId() + { + var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"].Should().Be("plaintext"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"]; + + Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached() + { + var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); + var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"]; + + Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"]; + + Guid.TryParse(secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid"); + secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnHashedMachineIdOld() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"].Should().NotBe("plaintext"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddressOld() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"]; + + Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnIsOutputRedirected() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Output Redirected"].Should().BeOneOf("True", "False"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnIsCIDetection() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Continuous Integration"].Should().BeOneOf("True", "False"); + } + + [Fact] + public void TelemetryCommonPropertiesShouldContainKernelVersion() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Kernel Version"].Should().Be(RuntimeInformation.OSDescription); + } + + [Fact] + public void TelemetryCommonPropertiesShouldContainArchitectureInformation() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["OS Architecture"].Should().Be(RuntimeInformation.OSArchitecture.ToString()); + } + + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainWindowsInstallType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().NotBeEmpty(); + } + + [UnixOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyWindowsInstallType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().BeEmpty(); + } + + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainWindowsProductType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().NotBeEmpty(); + } + + [UnixOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyWindowsProductType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().BeEmpty(); + } + + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty(); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty(); + } + + [MacOsOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion2() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty(); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty(); + } + + [LinuxOnlyFact] + public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() + { + if (!RuntimeInformation.RuntimeIdentifier.Contains("alpine", StringComparison.OrdinalIgnoreCase)) + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().NotBeEmpty(); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().NotBeEmpty(); + } + } + + [Fact] + public void TelemetryCommonPropertiesShouldReturnIsLLMDetection() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["llm"].Should().BeOneOf("claude", null); + } + + [Theory] + [MemberData(nameof(CITelemetryTestCases))] + public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected) + { + try + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, value); + } + new CIEnvironmentDetectorForTelemetry().IsCIEnvironment().Should().Be(expected); + } + finally + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, null); + } + } + } + + [Theory] + [MemberData(nameof(LLMTelemetryTestCases))] + public void CanDetectLLMStatusForEnvVars(Dictionary? envVars, string? expected) + { + try + { + if (envVars is not null) + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, value); + } + } + new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected); + } + finally + { + if (envVars is not null) + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, null); + } + } + } + } + + [Theory] + [InlineData("dummySessionId")] + [InlineData(null)] + public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) + { + var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); + var commonProperties = unitUnderTest.GetTelemetryCommonProperties(sessionId); + + commonProperties.Should().ContainKey("SessionId"); + commonProperties["SessionId"].Should().Be(sessionId); + } + + + public static TheoryData?, string?> LLMTelemetryTestCases => new() + { + { new Dictionary { {"CLAUDECODE", "1" } }, "claude" }, + { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" }, + { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, + { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" }, + { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, + { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" }, + { new Dictionary { { "CODEX_CLI", "1" } }, "codex" }, + { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" }, + { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" }, + { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" }, + { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" }, + { new Dictionary { { "OR_APP_NAME", "Plandex" } }, "plandex" }, + { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" }, + { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" }, + { new Dictionary { { "DROID_CLI", "true" } }, "droid" }, + { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" }, + { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" }, + { new Dictionary { { "ZED_TERM", "1" } }, "zed" }, + { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" }, + { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" }, + { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" }, + { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" }, + { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" }, + { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" }, + { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" }, + { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" }, + // Test combinations of older tools + { 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" }, + // Test combinations of newer tools + { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" }, + { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" }, + { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" }, + { new Dictionary { { "GEMINI_CLI", "false" } }, null }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null }, + { new Dictionary { { "AGENT_CLI", "false" } }, null }, + { new Dictionary { { "DROID_CLI", "false" } }, null }, + { new Dictionary { { "KIMI_CLI", "false" } }, null }, + { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null }, + { new Dictionary(), null }, + }; + + 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 + { + public string RunWithCache(string cacheKey, Func getValueToCache) + { + return getValueToCache(); + } + + public string RunWithCacheInFilePath(string cacheFilepath, Func getValueToCache) + { + return getValueToCache(); + } + } + } +} diff --git a/test/dotnet.Tests/TelemetryFilterTest.cs b/test/dotnet.Tests/TelemetryFilterTest.cs new file mode 100644 index 000000000000..db5f835e791d --- /dev/null +++ b/test/dotnet.Tests/TelemetryFilterTest.cs @@ -0,0 +1,211 @@ +// 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.Cli.Utils; +using Microsoft.DotNet.Utilities; +using Parser = Microsoft.DotNet.Cli.Parser; + +namespace Microsoft.DotNet.Tests +{ + /// + /// Only adding the performance data tests for now as the TelemetryCommandTests cover most other scenarios already + /// + public class TelemetryFilterTests : SdkTest + { + private readonly FakeRecordEventNameTelemetry _fakeTelemetry; + + public string EventName { get; set; } + + public IDictionary Properties { get; set; } + + public TelemetryFilterTests(ITestOutputHelper log) : base(log) + { + _fakeTelemetry = new FakeRecordEventNameTelemetry(); + TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent); + TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithoutPerformanceData() + { + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(parseResult); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + e.Measurement == null); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData() + { + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 12345 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + e.Measurement.ContainsKey("Startup Time") && + e.Measurement["Startup Time"] == 12345); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithGlobalJsonState() + { + string globalJsonState = "invalid_data"; + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary(), globalJsonState)); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + e.Measurement == null && + e.Properties.ContainsKey("globalJson") && + e.Properties["globalJson"] == Sha256Hasher.HashWithNormalizedCasing(globalJsonState)); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithZeroPerformanceData() + { + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + e.Measurement == null); + } + + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithSomeZeroPerformanceData() + { + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + !e.Measurement.ContainsKey("Startup Time") && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] == 23456); + } + + [Fact] + public void SubLevelCommandNameShouldBeSentToTelemetryWithoutPerformanceData() + { + var parseResult = Parser.Parse(["new", "console"]); + TelemetryEventEntry.SendFiltered(parseResult); + _fakeTelemetry + .LogEntries.Should() + .Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW") && + e.Measurement == null); + } + + [Fact] + public void SubLevelCommandNameShouldBeSentToTelemetryWithPerformanceData() + { + var parseResult = Parser.Parse(["new", "console"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 34567 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW") && + e.Measurement.ContainsKey("Startup Time") && + e.Measurement["Startup Time"] == 34567); + } + + [Fact] + public void SubLevelCommandNameShouldBeSentToTelemetryWithZeroPerformanceData() + { + var parseResult = Parser.Parse(["new", "console"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW") && + e.Measurement == null); + } + + [Fact] + public void SubLevelCommandNameShouldBeSentToTelemetryWithSomeZeroPerformanceData() + { + var parseResult = Parser.Parse(["new", "console"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 45678 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("argument") && + e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("NEW") && + !e.Measurement.ContainsKey("Startup Time") && + e.Measurement.ContainsKey("Parse Time") && + e.Measurement["Parse Time"] == 45678); + } + + [Fact] + public void WorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() + { + var parseResult = + Parser.Parse(["workload", "install", "microsoft-ios-sdk-full"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, + new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && + e.Properties["subcommand"] == + Sha256Hasher.Hash("INSTALL") && + e.Properties["argument"] == + Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL")); + } + + [Fact] + public void ToolsSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() + { + var parseResult = + Parser.Parse(["tool", "install", "dotnet-format"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, + new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("TOOL") && + e.Properties["subcommand"] == + Sha256Hasher.Hash("INSTALL") && + e.Properties["argument"] == + Sha256Hasher.Hash("DOTNET-FORMAT")); + } + + [Fact] + public void WhenCalledWithDiagnosticWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() + { + var parseResult = + Parser.Parse(["-d", "workload", "install", "microsoft-ios-sdk-full"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, + new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && + e.Properties["subcommand"] == + Sha256Hasher.Hash("INSTALL") && + e.Properties["argument"] == + Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL")); + } + + [Fact] + public void WhenCalledWithMissingArgumentWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() + { + var parseResult = + Parser.Parse(["-d", "workload", "install"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, + new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } })); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && + e.Properties["subcommand"] == + Sha256Hasher.Hash("INSTALL")); + } + } +} diff --git a/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs b/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs deleted file mode 100644 index 277e89826a8d..000000000000 --- a/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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.Concurrent; -using Microsoft.DotNet.Cli.Telemetry; - -namespace Microsoft.DotNet.Tests.TelemetryTests; - -public class FakeRecordEventNameTelemetry : ITelemetryClient -{ - public bool Enabled { get; set; } - - public string? EventName { get; set; } - - public void TrackEvent(string eventName, IDictionary? properties) - { - LogEntries.Add(new LogEntry - { - EventName = eventName, - Properties = properties ?? new Dictionary() - }); - } - - public ConcurrentBag LogEntries { get; set; } = []; - - public class LogEntry - { - public string? EventName { get; set; } - public IDictionary Properties { get; set; } = new Dictionary(); - } -} diff --git a/test/dotnet.Tests/TelemetryTests/SenderTests.cs b/test/dotnet.Tests/TelemetryTests/SenderTests.cs new file mode 100644 index 000000000000..6d6a4983ea23 --- /dev/null +++ b/test/dotnet.Tests/TelemetryTests/SenderTests.cs @@ -0,0 +1,184 @@ +// 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 System.Net; +using System.Runtime.CompilerServices; +using Moq; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel.Tests +{ + public class SenderTests : SdkTest + { + private int _deleteCount; + + private Mock TransmissionMock { get; } + + private Mock StorageBaseMock { get; } + + public SenderTests(ITestOutputHelper log) : base(log) + { + StorageBaseMock = new Mock(); + TransmissionMock = new Mock(string.Empty, new Uri("http://some/url"), new byte[] { }, + string.Empty, string.Empty); + _deleteCount = 0; + StorageBaseMock.Setup(storage => storage.Delete(It.IsAny())) + .Callback(() => _deleteCount++); + } + + [Fact] + public void WhenServerReturn503TransmissionWillBeRetried() + { + var Sender = GetSenderUnderTest(); + int peekCounts = 0; + + // Setup transmission.SendAsync() to throw WebException that has 503 status Code + TransmissionMock.Setup(transmission => transmission.SendAsync()) + .Throws(GenerateWebException((HttpStatusCode)503)); + + // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks. + StorageBaseMock.Setup(storage => storage.Peek()) + .Returns(TransmissionMock.Object) + .Callback(() => + { + if (peekCounts++ == 10) + { + Sender.StopAsync(); + } + }); + + // Act + Sender.SendLoop(); + _deleteCount.Should().Be(0, + "delete is not expected to be called on 503, request is expected to be send forever."); + } + + [Fact] + public void WhenServerReturn400IntervalWillBe10Seconds() + { + var Sender = GetSenderUnderTest(); + int peekCounts = 0; + + // Setup transmission.SendAsync() to throw WebException that has 400 status Code + TransmissionMock.Setup(transmission => transmission.SendAsync()) + .Throws(GenerateWebException((HttpStatusCode)400)); + + // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks. + StorageBaseMock.Setup(storage => storage.Peek()) + .Returns(TransmissionMock.Object) + .Callback(() => + { + if (peekCounts++ == 10) + { + Sender.StopAsync(); + } + }); + + // Cache the interval (it is a parameter passed to the Send method). + TimeSpan intervalOnSixIteration = TimeSpan.Zero; + Sender.OnSend = interval => intervalOnSixIteration = interval; + + // Act + Sender.SendLoop(); + + intervalOnSixIteration.TotalSeconds.Should().Be(5); + _deleteCount.Should().Be(10, "400 should not be retried so delete should always be called."); + } + + [Fact] + public void DisposeDoesNotThrow() + { + new Sender(StorageBaseMock.Object, + new PersistenceTransmitter( + CreateStorageService(), + 3)) + .Dispose(); + } + + [Fact] + public void WhenServerReturnDnsErrorRequestWillBeRetried() + { + var Sender = GetSenderUnderTest(); + int peekCounts = 0; + + // Setup transmission.SendAsync() to throw WebException with ProxyNameResolutionFailure failure + WebException webException = new( + string.Empty, + new Exception(), + WebExceptionStatus.ProxyNameResolutionFailure, + null); + TransmissionMock.Setup(transmission => transmission.SendAsync()).Throws(webException); + + // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks. + StorageBaseMock.Setup(storage => storage.Peek()) + .Returns(TransmissionMock.Object) + .Callback(() => + { + if (peekCounts++ == 10) + { + Sender.StopAsync(); + } + }); + + // Act + Sender.SendLoop(); + + _deleteCount.Should().Be(0, + "delete is not expected to be called on Dns errors since it , request is expected to be retried forever."); + } + + private WebException GenerateWebException(HttpStatusCode httpStatusCode) + { + Mock httpWebResponse = new(); + httpWebResponse.SetupGet(webResponse => webResponse.StatusCode).Returns(httpStatusCode); + + WebException webException = new(string.Empty, new Exception(), WebExceptionStatus.SendFailure, + httpWebResponse.Object); + + return webException; + } + + /// + /// A class that inherits from Sender, to expose its protected methods. + /// + internal class SenderUnderTest : Sender + { + internal Action OnSend = nextSendInterval => { }; + + internal SenderUnderTest(BaseStorageService storage, PersistenceTransmitter transmitter) + : base(storage, transmitter, false) + { + } + + internal AutoResetEvent IntervalAutoResetEvent => DelayHandler; + + internal new void SendLoop() + { + base.SendLoop(); + } + + protected override bool Send(StorageTransmission transmission, ref TimeSpan nextSendInterval) + { + OnSend(nextSendInterval); + DelayHandler.Set(); + return base.Send(transmission, ref nextSendInterval); + } + } + + private StorageService CreateStorageService([CallerMemberName] string testName = null) + { + string tempPath = Path.Combine(TestAssetsManager.CreateTestDirectory("TestStorageService", identifier: testName).Path, Path.GetTempFileName()); + StorageService storageService = new(); + storageService.Init(tempPath); + return storageService; + } + + private SenderUnderTest GetSenderUnderTest([CallerMemberName] string testName = null) + { + StorageService storageService = CreateStorageService(testName); + PersistenceTransmitter transmitter = new(storageService, 0); + return new SenderUnderTest(StorageBaseMock.Object, transmitter); + } + } +} diff --git a/test/dotnet.Tests/TelemetryTests/StorageTests.cs b/test/dotnet.Tests/TelemetryTests/StorageTests.cs new file mode 100644 index 000000000000..bd56db879b8d --- /dev/null +++ b/test/dotnet.Tests/TelemetryTests/StorageTests.cs @@ -0,0 +1,202 @@ +// 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 System.Runtime.CompilerServices; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility.Implementation; +using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry; + +namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel.Tests +{ + /// + /// Tests for Storage. + /// + /// + /// To reduce complexity, there was a design decision to make Storage the file system abstraction layer. + /// That means that Storage knows about the file system types (e.g. IStorageFile or FileInfo). + /// Those types are not easy to mock (even IStorageFile is using extension methods that makes it very hard to mock). + /// Therefore those UnitTests just doesn't mock the file system. Every unit test in + /// reads and writes files to/from the disk. + /// + public class StorageTests : SdkTest + { + public StorageTests(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public async Task EnqueuedContentIsEqualToPeekedContent() + { + // Setup + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + Transmission transmissionToEnqueue = CreateTransmission(new TraceTelemetry("mock_item")); + + // Act + await storage.EnqueueAsync(transmissionToEnqueue); + StorageTransmission peekedTransmission = storage.Peek(); + + // Asserts + string enqueuedContent = + Encoding.UTF8.GetString(transmissionToEnqueue.Content, 0, transmissionToEnqueue.Content.Length); + string peekedContent = + Encoding.UTF8.GetString(peekedTransmission.Content, 0, peekedTransmission.Content.Length); + enqueuedContent.Should().Be(peekedContent); + } + + [Fact] + public void DeletedItemIsNotReturnedInCallsToPeek() + { + // Setup - create a storage with one item + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + Transmission transmissionToEnqueue = CreateTransmissionAndEnqueueIt(storage); + + // Act + StorageTransmission firstPeekedTransmission; + + // if item is not disposed,peek will not return it (regardless of the call to delete). + // So for this test to actually test something, using 'using' is required. + using (firstPeekedTransmission = storage.Peek()) + { + storage.Delete(firstPeekedTransmission); + } + + StorageTransmission secondPeekedTransmission = storage.Peek(); + + // Asserts + firstPeekedTransmission.Should().NotBeNull(); + secondPeekedTransmission.Should().BeNull(); + } + + [Fact] + public void PeekedItemIsOnlyReturnedOnce() + { + // Setup - create a storage with one item + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + + Transmission transmissionToEnqueue = CreateTransmissionAndEnqueueIt(storage); + + // Act + StorageTransmission firstPeekedTransmission = storage.Peek(); + StorageTransmission secondPeekedTransmission = storage.Peek(); + + // Asserts + firstPeekedTransmission.Should().NotBeNull(); + secondPeekedTransmission.Should().BeNull(); + } + + [Fact] + public async Task PeekedItemIsReturnedAgainAfterTheItemInTheFirstCallToPeekIsDisposed() + { + // Setup - create a storage with one item + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + + Transmission transmissionToEnqueue = CreateTransmission(new TraceTelemetry("mock_item")); + await storage.EnqueueAsync(transmissionToEnqueue); + + // Act + StorageTransmission firstPeekedTransmission; + using (firstPeekedTransmission = storage.Peek()) + { + } + + StorageTransmission secondPeekedTransmission = storage.Peek(); + + // Asserts + firstPeekedTransmission.Should().NotBeNull(); + secondPeekedTransmission.Should().NotBeNull(); + } + + [Fact] + public void WhenStorageHasTwoItemsThenTwoCallsToPeekReturns2DifferentItems() + { + // Setup - create a storage with 2 items + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + + Transmission firstTransmission = CreateTransmissionAndEnqueueIt(storage); + Transmission secondTransmission = CreateTransmissionAndEnqueueIt(storage); + + // Act + StorageTransmission firstPeekedTransmission = storage.Peek(); + StorageTransmission secondPeekedTransmission = storage.Peek(); + + // Asserts + firstPeekedTransmission.Should().NotBeNull(); + secondPeekedTransmission.Should().NotBeNull(); + + string first = Encoding.UTF8.GetString(firstPeekedTransmission.Content, 0, + firstPeekedTransmission.Content.Length); + string second = Encoding.UTF8.GetString(secondPeekedTransmission.Content, 0, + secondPeekedTransmission.Content.Length); + first.Should().NotBe(second); + } + + [Fact] + public void WhenMaxFilesIsOneThenSecondTransmissionIsDropped() + { + // Setup + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + + storage.MaxFiles = 1; + + // Act - Enqueue twice + CreateTransmissionAndEnqueueIt(storage); + CreateTransmissionAndEnqueueIt(storage); + + // Asserts - Second Peek should be null + storage.Peek().Should().NotBeNull(); + storage.Peek().Should().BeNull(); + } + + [Fact] + public void WhenMaxSizeIsReachedThenEnqueuedTransmissionsAreDropped() + { + // Setup - create a storage with 2 items + StorageService storage = new(); + storage.Init(GetTemporaryPath()); + + storage.CapacityInBytes = 200; // Each file enqueued in CreateTransmissionAndEnqueueIt is ~300 bytes. + + // Act - Enqueue twice + CreateTransmissionAndEnqueueIt(storage); + CreateTransmissionAndEnqueueIt(storage); + + // Asserts - Second Peek should be null + storage.Peek().Should().NotBeNull(); + storage.Peek().Should().BeNull(); + } + + private static Transmission CreateTransmission(IChannelTelemetry telemetry) + { + byte[] data = JsonSerializer.Serialize(new[] { telemetry }); + Transmission transmission = new( + new Uri(@"http://some.url"), + data, + "application/x-json-stream", + JsonSerializer.CompressionType); + + return transmission; + } + + private static Transmission CreateTransmissionAndEnqueueIt(StorageService storage) + { + Transmission firstTransmission = CreateTransmission(new TraceTelemetry(Guid.NewGuid().ToString())); + storage.EnqueueAsync(firstTransmission).ConfigureAwait(false).GetAwaiter().GetResult(); + + return firstTransmission; + } + + private string GetTemporaryPath([CallerMemberName] string callingMethod = null) + { + return TestAssetsManager.CreateTestDirectory(callingMethod).Path; + } + } +} diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs b/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs deleted file mode 100644 index eb64306bfa69..000000000000 --- a/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// 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.Nodes; - -namespace Microsoft.DotNet.Tests.TelemetryTests; - -public class TelemetryClientTests(ITestOutputHelper log) : SdkTest(log) -{ - public static TheoryData CommandsWithExitCode => new() - { - { new[] { "--help" }, "0" }, - { new[] { "--info" }, "0" }, - { new[] { "workload", "list" }, "0" }, - { new[] { "sdk", "check" }, "0" }, - { new[] { "build-server", "shutdown" }, "0" }, - { new[] { "solution", "list" }, "1" }, - { new[] { "clean" }, "1" }, - { new[] { "run" }, "1" }, - { new[] { "new", "details" }, "127" } - }; - - // Only runs on Windows because OTel libraries are only referenced on Windows builds. - // Thus, this test that writes telemetry logs will not work on other platforms. - [PlatformSpecificTheory(TestPlatforms.Windows)] - [MemberData(nameof(CommandsWithExitCode))] - public void ItProcessesTelemetryData(string[] commandArgs, string exitCodeExpected) - { - var testDir = TestAssetsManager.CreateTestDirectory().Path; - var commandString = string.Join(' ', commandArgs); - var logFile = Path.Combine(testDir, $"TelemLog_{commandString}.json"); - - new DotnetCommand(Log, commandArgs) - .WithWorkingDirectory(testDir) - .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "false") - .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT", "true") - .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_LOG_PATH", logFile) - .Execute(); - - var logFileInfo = new FileInfo(logFile); - logFileInfo.Should().Exist(); - - var telemetryJson = JsonNode.Parse(logFileInfo.ReadAllText()); - telemetryJson.Should().NotBeNull(); - - var activities = telemetryJson["activities"]?.AsArray(); - activities.Should().NotBeNull(); - - var mainOperation = activities.FirstOrDefault(n => n?["operationName"]?.GetValue() == "main"); - mainOperation.Should().NotBeNull(); - - var displayName = mainOperation["displayName"]?.GetValue(); - displayName.Should().Be($"dotnet {commandString}"); - - var events = mainOperation["events"]?.AsArray(); - events.Should().NotBeNull(); - - var finishEvent = events.FirstOrDefault(n => n?["name"]?.GetValue() == "dotnet/cli/command/finish"); - finishEvent.Should().NotBeNull(); - - var tags = finishEvent["tags"]; - tags.Should().NotBeNull(); - - var exitCode = tags["exitCode"]?.GetValue(); - exitCode.Should().Be(exitCodeExpected); - } -} diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs deleted file mode 100644 index cef125282576..000000000000 --- a/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs +++ /dev/null @@ -1,332 +0,0 @@ -// 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.Cli.Commands.Hidden.InternalReportInstallSuccess; -using Microsoft.DotNet.Cli.Telemetry; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Utilities; - -namespace Microsoft.DotNet.Tests.TelemetryTests; - -[Collection(TestConstants.UsesStaticTelemetryState)] -public class TelemetryCommandTests : SdkTest -{ - private readonly FakeRecordEventNameTelemetry _fakeTelemetry; - - public string? EventName { get; set; } - - public IDictionary Properties { get; set; } = new Dictionary(); - - public TelemetryCommandTests(ITestOutputHelper log) : base(log) - { - _fakeTelemetry = new FakeRecordEventNameTelemetry(); - TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent); - TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); - } - - [Fact] - public void NoTelemetryIfCommandIsInvalid() - { - string[] args = { "publish", "-r" }; - Action a = () => { Cli.Program.ProcessArgsAndExecute(args); }; - a.Should().NotThrow(); - } - - [Fact] - public void NoTelemetryIfCommandIsInvalid2() - { - string[] args = { "restore", "-v" }; - Action a = () => { Cli.Program.ProcessArgsAndExecute(args); }; - a.Should().NotThrow(); - } - - [Fact] - public void TopLevelCommandNameShouldBeSentToTelemetry() - { - string[] args = { "help" }; - Cli.Program.ProcessArgsAndExecute(args); - - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("HELP")); - } - - [Fact] - public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "console"; - string[] args = { "new", argumentToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("NEW")); - } - - [Fact] - public void DotnetHelpCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "something"; - string[] args = { "help", argumentToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("HELP")); - } - - [Fact] - public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "package"; - string[] args = { "add", argumentToSend, "aPackageName" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("ADD")); - } - - [Fact] - public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry2() - { - const string argumentToSend = "reference"; - string[] args = { "add", argumentToSend, "aPackageName" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("ADD")); - } - - [Fact] - public void DotnetRemoveCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "package"; - string[] args = { "remove", argumentToSend, "aPackageName" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("REMOVE")); - } - - [Fact] - public void DotnetListCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "reference"; - string[] args = { "list", argumentToSend, "aPackageName" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("LIST")); - } - - [Fact] - public void DotnetSlnCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "list"; - string[] args = { "sln", "aSolution", argumentToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("SOLUTION")); - } - - [Fact] - public void DotnetNugetCommandFirstArgumentShouldBeSentToTelemetry() - { - const string argumentToSend = "push"; - - string[] args = { "nuget", argumentToSend, "path" }; - - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("NUGET")); - } - - [Fact(Skip = "https://github.com/dotnet/sdk/issues/47862")] - public void DotnetNewCommandLanguageOpinionShouldBeSentToTelemetry() - { - const string optionKey = "language"; - const string optionValueToSend = "c#"; - string[] args = { "new", "console", "--" + optionKey, optionValueToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey(optionKey) && - e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("NEW")); - } - - [Fact] - public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetry() - { - const string optionKey = "verbosity"; - const string optionValueToSend = "minimal"; - string[] args = { "restore", "--" + optionKey, optionValueToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey(optionKey) && - e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("RESTORE")); - } - - [Fact] - public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetry() - { - const string optionKey = "configuration"; - const string optionValueToSend = "Debug"; - string[] args = { "build", "--" + optionKey, optionValueToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey(optionKey) && - e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); - } - - [Fact] - public void DotnetPublishCommandRuntimeOpinionsShouldBeSentToTelemetry() - { - const string optionKey = "runtime"; - const string optionValueToSend = $"{ToolsetInfo.LatestWinRuntimeIdentifier}-x64"; - string[] args = { "publish", "--" + optionKey, optionValueToSend }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey(optionKey) && - e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("PUBLISH")); - } - - [Fact] - public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption() - { - string[] args = { "build", "--configuration", "Debug", "--runtime", $"{ToolsetInfo.LatestMacRuntimeIdentifier}-x64" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") && - e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); - - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("runtime") && - e.Properties["runtime"] == Sha256Hasher.Hash($"{ToolsetInfo.LatestMacRuntimeIdentifier.ToUpper()}-X64") && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); - } - - [Fact] - public void DotnetRunCleanTestCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption() - { - string[] args = { "clean", "--configuration", "Debug", "--framework", ToolsetInfo.CurrentTargetFramework }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") && - e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("CLEAN")); - - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("framework") && - e.Properties["framework"] == Sha256Hasher.Hash(ToolsetInfo.CurrentTargetFramework.ToUpper()) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("CLEAN")); - } - - [Fact] - public void DotnetUpdatePackageVulnerableOptionShouldBeSentToTelemetry() - { - const string optionKey = "vulnerable"; - string[] args = { "package", "update", "--vulnerable" }; - Cli.Program.ProcessArgsAndExecute(args); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey(optionKey) && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("PACKAGE UPDATE")); - } - - [WindowsOnlyFact] - public void InternalreportinstallsuccessCommandCollectExeNameWithEventname() - { - FakeRecordEventNameTelemetry fakeTelemetry = new(); - string[] args = { "c:\\mypath\\dotnet-sdk-latest-win-x64.exe" }; - - InternalReportInstallSuccessCommand.ProcessInputAndSendTelemetry(args, fakeTelemetry); - - fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "install/reportsuccess" && e.Properties.ContainsKey("exeName") && - e.Properties["exeName"] == Sha256Hasher.Hash("DOTNET-SDK-LATEST-WIN-X64.EXE")); - } - - [Fact] - public void ExceptionShouldBeSentToTelemetry() - { - Exception? caughtException = null; - try - { - string[] args = { "build" }; - Cli.Program.ProcessArgsAndExecute(args); - throw new ArgumentException("test exception"); - } - catch (Exception ex) - { - caughtException = ex; - TelemetryEventEntry.SendFiltered(ex); - } - - var stackTrace = caughtException?.StackTrace ?? string.Empty; - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "mainCatchException/exception" && - e.Properties.ContainsKey("exceptionType") && - e.Properties["exceptionType"] == "System.ArgumentException" && - e.Properties.ContainsKey("detail") && - e.Properties["detail"] != null && - e.Properties["detail"]!.Contains(stackTrace)); - } -} diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs deleted file mode 100644 index fb4382e0ae04..000000000000 --- a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs +++ /dev/null @@ -1,311 +0,0 @@ -// 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.Cli.Telemetry; -using Microsoft.DotNet.Configurer; - -namespace Microsoft.DotNet.Tests.TelemetryTests; - -public class TelemetryCommonPropertiesTests : SdkTest -{ - public TelemetryCommonPropertiesTests(ITestOutputHelper log) : base(log) - { - } - - [Fact] - public void TelemetryCommonPropertiesShouldContainIfItIsInDockerOrNot() - { - var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId").Should().ContainKey("Docker Container"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnHashedPath() - { - var unitUnderTest = new TelemetryCommonProperties(() => "ADirectory", userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Current Path Hash"].Should().NotBe("ADirectory"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnHashedMachineId() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"].Should().NotBe("plaintext"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnDevDeviceId() - { - var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"].Should().Be("plaintext"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"]; - - Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached() - { - var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); - var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"]; - - Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid"); - var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"]; - - Guid.TryParse((string?)secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid"); - secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnHashedMachineIdOld() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"].Should().NotBe("plaintext"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddressOld() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"]; - - Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnIsOutputRedirected() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Output Redirected"].Should().BeOneOf("True", "False"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnIsCIDetection() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Continuous Integration"].Should().BeOneOf("True", "False"); - } - - [Fact] - public void TelemetryCommonPropertiesShouldContainKernelVersion() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Kernel Version"].Should().Be(RuntimeInformation.OSDescription); - } - - [Fact] - public void TelemetryCommonPropertiesShouldContainArchitectureInformation() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["OS Architecture"].Should().Be(RuntimeInformation.OSArchitecture.ToString()); - } - - [WindowsOnlyFact] - public void TelemetryCommonPropertiesShouldContainWindowsInstallType() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().NotBeEmpty(); - } - - [UnixOnlyFact] - public void TelemetryCommonPropertiesShouldContainEmptyWindowsInstallType() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().BeEmpty(); - } - - [WindowsOnlyFact] - public void TelemetryCommonPropertiesShouldContainWindowsProductType() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().NotBeEmpty(); - } - - [UnixOnlyFact] - public void TelemetryCommonPropertiesShouldContainEmptyWindowsProductType() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().BeEmpty(); - } - - [WindowsOnlyFact] - public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty(); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty(); - } - - [MacOsOnlyFact] - public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion2() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty(); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty(); - } - - [LinuxOnlyFact] - public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() - { - if (!RuntimeInformation.RuntimeIdentifier.Contains("alpine", StringComparison.OrdinalIgnoreCase)) - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().NotBeEmpty(); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().NotBeEmpty(); - } - } - - [Fact] - public void TelemetryCommonPropertiesShouldReturnIsLLMDetection() - { - var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["llm"].Should().BeOneOf("claude", null); - } - - [Theory] - [MemberData(nameof(CITelemetryTestCases))] - public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected) - { - try - { - foreach (var (key, value) in envVars) - { - Environment.SetEnvironmentVariable(key, value); - } - new CIEnvironmentDetectorForTelemetry().IsCIEnvironment().Should().Be(expected); - } - finally - { - foreach (var (key, value) in envVars) - { - Environment.SetEnvironmentVariable(key, null); - } - } - } - - [Theory] - [MemberData(nameof(LLMTelemetryTestCases))] - public void CanDetectLLMStatusForEnvVars(Dictionary? envVars, string? expected) - { - try - { - if (envVars is not null) - { - foreach (var (key, value) in envVars) - { - Environment.SetEnvironmentVariable(key, value); - } - } - new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected); - } - finally - { - if (envVars is not null) - { - foreach (var (key, value) in envVars) - { - Environment.SetEnvironmentVariable(key, null); - } - } - } - } - - [Theory] - [InlineData("dummySessionId")] - [InlineData(null)] - public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) - { - var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); - var commonProperties = unitUnderTest.GetTelemetryCommonProperties(sessionId); - - commonProperties.Should().ContainKey("SessionId"); - commonProperties["SessionId"].Should().Be(sessionId); - } - - public static TheoryData?, string?> LLMTelemetryTestCases => new() - { - { new Dictionary { {"CLAUDECODE", "1" } }, "claude" }, - { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" }, - { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, - { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" }, - { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, - { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, - { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" }, - { new Dictionary { { "CODEX_CLI", "1" } }, "codex" }, - { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" }, - { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" }, - { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" }, - { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" }, - { new Dictionary { { "OR_APP_NAME", "Plandex" } }, "plandex" }, - { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" }, - { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" }, - { new Dictionary { { "DROID_CLI", "true" } }, "droid" }, - { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" }, - { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" }, - { new Dictionary { { "ZED_TERM", "1" } }, "zed" }, - { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" }, - { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" }, - { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" }, - { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" }, - { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" }, - { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" }, - { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" }, - { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" }, - // Test combinations of older tools - { 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" }, - // Test combinations of newer tools - { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" }, - { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" }, - { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" }, - { new Dictionary { { "GEMINI_CLI", "false" } }, null }, - { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null }, - { new Dictionary { { "AGENT_CLI", "false" } }, null }, - { new Dictionary { { "DROID_CLI", "false" } }, null }, - { new Dictionary { { "KIMI_CLI", "false" } }, null }, - { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null }, - { new Dictionary(), null }, - }; - - 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 - { - public string RunWithCache(string cacheKey, Func getValueToCache) - { - return getValueToCache(); - } - - public string RunWithCacheInFilePath(string cacheFilepath, Func getValueToCache) - { - return getValueToCache(); - } - } -} diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs b/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs deleted file mode 100644 index 300f1f90e0e1..000000000000 --- a/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs +++ /dev/null @@ -1,123 +0,0 @@ -// 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.Cli.Telemetry; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Utilities; -using Parser = Microsoft.DotNet.Cli.Parser; - -namespace Microsoft.DotNet.Tests.TelemetryTests; - -/// -/// Only adding the performance data tests for now as the TelemetryCommandTests cover most other scenarios already -/// -public class TelemetryFilterTests : SdkTest -{ - private readonly FakeRecordEventNameTelemetry _fakeTelemetry; - - public string? EventName { get; set; } - - public IDictionary Properties { get; set; } = new Dictionary(); - - public TelemetryFilterTests(ITestOutputHelper log) : base(log) - { - _fakeTelemetry = new FakeRecordEventNameTelemetry(); - TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent); - TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); - } - - [Fact] - public void TopLevelCommandNameShouldBeSentToTelemetry() - { - var parseResult = Parser.Parse(["build"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("BUILD")); - } - - [Fact] - public void TopLevelCommandNameShouldBeSentToTelemetryWithGlobalJsonState() - { - string globalJsonState = "invalid_data"; - var parseResult = Parser.Parse(["build"]); - TelemetryEventEntry.SendFiltered(new ParseResultWithGlobalJsonState(parseResult, globalJsonState)); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && - e.Properties.ContainsKey("globalJson") && - e.Properties["globalJson"] == Sha256Hasher.HashWithNormalizedCasing(globalJsonState)); - } - - [Fact] - public void SubLevelCommandNameShouldBeSentToTelemetry() - { - var parseResult = Parser.Parse(["new", "console"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry - .LogEntries.Should() - .Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("argument") && - e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("NEW")); - } - - [Fact] - public void WorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() - { - var parseResult = - Parser.Parse(["workload", "install", "microsoft-ios-sdk-full"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && - e.Properties["subcommand"] == - Sha256Hasher.Hash("INSTALL") && - e.Properties["argument"] == - Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL")); - } - - [Fact] - public void ToolsSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() - { - var parseResult = - Parser.Parse(["tool", "install", "dotnet-format"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("TOOL") && - e.Properties["subcommand"] == - Sha256Hasher.Hash("INSTALL") && - e.Properties["argument"] == - Sha256Hasher.Hash("DOTNET-FORMAT")); - } - - [Fact] - public void WhenCalledWithDiagnosticWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() - { - var parseResult = - Parser.Parse(["-d", "workload", "install", "microsoft-ios-sdk-full"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && - e.Properties["subcommand"] == - Sha256Hasher.Hash("INSTALL") && - e.Properties["argument"] == - Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL")); - } - - [Fact] - public void WhenCalledWithMissingArgumentWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry() - { - var parseResult = - Parser.Parse(["-d", "workload", "install"]); - TelemetryEventEntry.SendFiltered(parseResult); - _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" && - e.Properties.ContainsKey("verb") && - e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") && - e.Properties["subcommand"] == - Sha256Hasher.Hash("INSTALL")); - } -} From 98c1736db537514207dd792bea311f218b5b182b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 9 Apr 2026 18:56:04 +0000 Subject: [PATCH 3/3] Revert "[main] Source code updates from dotnet/dotnet (#53766)" This reverts commit 045dea9ffd75a90a7a270f0019ff508fdc4bca04, reversing changes made to 35959f23bcd3535fbb8a91beaaac1f370f279855. --- eng/Version.Details.props | 224 ++++++++-------- eng/Version.Details.xml | 538 +++++++++++++++++++------------------- global.json | 4 +- 3 files changed, 383 insertions(+), 383 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index b3a969212d8f..f1955acb69a7 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -8,103 +8,103 @@ This file should be imported by eng/Versions.props 2.1.0 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 10.0.0-preview.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 10.0.0-preview.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 18.6.0-preview-26208-110 18.6.0-preview-26208-110 7.6.0-rc.20910 - 11.0.100-preview.4.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 10.0.0-preview.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 2.0.0-preview.1.26208.110 - 3.0.0-preview.4.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-beta.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 15.2.100-preview4.26208.110 - 11.0.0-preview.4.26208.110 - 5.7.0-1.26208.110 - 5.7.0-1.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 + 11.0.100-preview.4.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 10.0.0-preview.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 2.0.0-preview.1.26208.106 + 3.0.0-preview.4.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-beta.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 15.2.100-preview4.26208.106 + 11.0.0-preview.4.26208.106 + 5.7.0-1.26208.106 + 5.7.0-1.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 10.0.0-preview.7.25377.103 - 10.0.0-preview.26208.110 - 11.0.0-preview.4.26208.110 + 10.0.0-preview.26208.106 + 11.0.0-preview.4.26208.106 18.7.0-preview-26208-110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 - 11.0.100-preview.4.26208.110 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 + 11.0.100-preview.4.26208.106 18.7.0-preview-26208-110 18.7.0-preview-26208-110 - 3.3.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 + 3.3.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 7.6.0-rc.20910 7.6.0-rc.20910 7.6.0-rc.20910 @@ -121,28 +121,28 @@ This file should be imported by eng/Versions.props 7.6.0-rc.20910 7.6.0-rc.20910 7.6.0-rc.20910 - 11.0.0-preview.4.26208.110 - 3.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 - 11.0.0-preview.4.26208.110 + 11.0.0-preview.4.26208.106 + 3.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 + 11.0.0-preview.4.26208.106 2.3.0-preview.26203.3 4.3.0-preview.26203.3 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c208ab2eb76a..94ae00970131 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,62 +1,62 @@ - + - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d @@ -68,174 +68,174 @@ https://github.com/dotnet/dotnet 6a953e76162f3f079405f80e28664fa51b136740 - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d https://github.com/microsoft/testfx @@ -577,9 +577,9 @@ https://github.com/microsoft/testfx 1ddd2f1a558e9c79b5327c5ccc0e9e89df39d4da - + https://github.com/dotnet/dotnet - 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2 + e52493553982a80cd4c7d372c104e3a9dbff0e0d diff --git a/global.json b/global.json index 6caa2e64febd..d467aee51da9 100644 --- a/global.json +++ b/global.json @@ -21,8 +21,8 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26208.110", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26208.110", + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26208.106", + "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26208.106", "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.4.0", "Microsoft.WixToolset.Sdk": "5.0.2-dotnet.2811440"