From c173b9ee1007344045a3802fb333beff94ff95a9 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 12 Jul 2025 09:33:25 -0500 Subject: [PATCH 01/19] Minimal work to make System.Diagnostics.Activity usable by the dotnet CLI application and codebase --- Directory.Packages.props | 1 + eng/Versions.props | 1 + .../Microsoft.DotNet.Cli.Utils/Activities.cs | 20 ++++ .../ForwardingAppImplementation.cs | 6 +- .../MSBuildForwardingAppWithoutLogging.cs | 8 +- .../Microsoft.DotNet.Cli.Utils.csproj | 1 + .../Commands/MSBuild/MSBuildForwardingApp.cs | 2 +- src/Cli/dotnet/Telemetry/Telemetry.cs | 108 +++++++++++++----- .../Telemetry/TelemetryCommonProperties.cs | 5 +- 9 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 104dedfd9eaf..abe2e9e23b4c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -108,6 +108,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index 99e97cedaae8..1b71e21ebfc6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -119,6 +119,7 @@ 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs new file mode 100644 index 000000000000..9f4665572436 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Utils; + +/// +/// Contains helpers for working with Activities in the .NET CLI. +/// +public static class Activities +{ + + /// + /// The main entrypoint for creating Activities in the .NET CLI. + /// All activities created in the CLI should use this , to allow + /// consumers to easily filter and trace CLI activities. + /// + public static ActivitySource Source { get; } = new("dotnet-cli", Product.Version); +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs index f6acb60ea0a1..1b2058d17744 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs @@ -19,7 +19,7 @@ internal class ForwardingAppImplementation private readonly string? _depsFile; private readonly string? _runtimeConfig; private readonly string? _additionalProbingPath; - private Dictionary _environmentVariables; + private Dictionary _environmentVariables; private readonly string[] _allArgs; @@ -29,7 +29,7 @@ public ForwardingAppImplementation( string? depsFile = null, string? runtimeConfig = null, string? additionalProbingPath = null, - Dictionary? environmentVariables = null) + Dictionary? environmentVariables = null) { _forwardApplicationPath = forwardApplicationPath; _argsToForward = argsToForward; @@ -86,7 +86,7 @@ public ProcessStartInfo GetProcessStartInfo() return processInfo; } - public ForwardingAppImplementation WithEnvironmentVariable(string name, string value) + public ForwardingAppImplementation WithEnvironmentVariable(string name, string? value) { _environmentVariables.Add(name, value); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs index 2e95365623a0..64e6fea0ccf9 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs @@ -39,7 +39,7 @@ public static string MSBuildVersion // True if, given current state of the class, MSBuild would be executed in its own process. public bool ExecuteMSBuildOutOfProc => _forwardingApp != null; - private readonly Dictionary _msbuildRequiredEnvironmentVariables = GetMSBuildRequiredEnvironmentVariables(); + private readonly Dictionary _msbuildRequiredEnvironmentVariables = GetMSBuildRequiredEnvironmentVariables(); private readonly List _msbuildRequiredParameters = [ "-maxcpucount", "--verbosity:m" ]; @@ -112,7 +112,7 @@ private static string EmitProperty(KeyValuePair property, string : $"--{label}:{property.Key}={property.Value}"; } - public void EnvironmentVariable(string name, string value) + public void EnvironmentVariable(string name, string? value) { if (_forwardingApp != null) { @@ -153,7 +153,7 @@ public int ExecuteInProc(string[] arguments) Dictionary savedEnvironmentVariables = []; try { - foreach (KeyValuePair kvp in _msbuildRequiredEnvironmentVariables) + foreach (KeyValuePair kvp in _msbuildRequiredEnvironmentVariables) { savedEnvironmentVariables[kvp.Key] = Environment.GetEnvironmentVariable(kvp.Key); Environment.SetEnvironmentVariable(kvp.Key, kvp.Value); @@ -218,7 +218,7 @@ private static string GetDotnetPath() return new Muxer().MuxerPath; } - internal static Dictionary GetMSBuildRequiredEnvironmentVariables() + internal static Dictionary GetMSBuildRequiredEnvironmentVariables() { return new() { 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 115f5dd125ec..3c0f616409d4 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 @@ -54,6 +54,7 @@ + diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs index f3ef9d93a44f..67c2a47f1051 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs @@ -59,7 +59,7 @@ public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null, public IEnumerable MSBuildArguments { get { return _forwardingAppWithoutLogging.GetAllArguments(); } } - public void EnvironmentVariable(string name, string value) + public void EnvironmentVariable(string name, string? value) { _forwardingAppWithoutLogging.EnvironmentVariable(name, value); } diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs index 28851da921b1..f908d01e7fe0 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -1,8 +1,7 @@ // 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 System.Diagnostics; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; @@ -14,13 +13,13 @@ namespace Microsoft.DotNet.Cli.Telemetry; public class Telemetry : ITelemetry { - internal static string CurrentSessionId = null; + internal static string? CurrentSessionId = null; internal static bool DisabledForTests = false; private readonly int _senderCount; - private TelemetryClient _client = null; - private Dictionary _commonProperties = null; - private Dictionary _commonMeasurements = null; - private Task _trackEventTask = null; + 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"; @@ -28,13 +27,13 @@ public class Telemetry : ITelemetry public Telemetry() : this(null) { } - public Telemetry(IFirstTimeUseNoticeSentinel sentinel) : this(sentinel, null) { } + public Telemetry(IFirstTimeUseNoticeSentinel? sentinel) : this(sentinel, null) { } public Telemetry( - IFirstTimeUseNoticeSentinel sentinel, - string sessionId, + IFirstTimeUseNoticeSentinel? sentinel, + string? sessionId, bool blockThreadInitialization = false, - IEnvironmentProvider environmentProvider = null, + IEnvironmentProvider? environmentProvider = null, int senderCount = 3) { @@ -78,7 +77,7 @@ internal static void EnableForTests() DisabledForTests = false; } - private static bool PermissionExists(IFirstTimeUseNoticeSentinel sentinel) + private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel) { if (sentinel == null) { @@ -97,9 +96,17 @@ public void TrackEvent(string eventName, IDictionary properties, } //continue the task in different threads - _trackEventTask = _trackEventTask.ContinueWith( - x => TrackEventTask(eventName, properties, measurements) - ); + if (_trackEventTask == null) + { + _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements)); + return; + } + else + { + _trackEventTask = _trackEventTask.ContinueWith( + x => TrackEventTask(eventName, properties, measurements) + ); + } } public void Flush() @@ -148,7 +155,7 @@ private void InitializeTelemetry() _client.Context.Device.OperatingSystem = CLIRuntimeEnvironment.OperatingSystem; _commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(); - _commonMeasurements = []; + _commonMeasurements = FrozenDictionary.Empty; } catch (Exception e) { @@ -170,12 +177,14 @@ private void TrackEventTask( try { - Dictionary eventProperties = GetEventProperties(properties); - Dictionary eventMeasurements = GetEventMeasures(measurements); + 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) { @@ -183,26 +192,69 @@ private void TrackEventTask( } } - private static string PrependProducerNamespace(string eventName) + private static ActivityEvent CreateActivityEvent( + string eventName, + IDictionary? properties, + IDictionary? measurements) { - return "dotnet/cli/" + eventName; + var tags = MakeTags(properties, measurements); + return new ActivityEvent( + PrependProducerNamespace(eventName), + tags: tags); } - private Dictionary GetEventMeasures(IDictionary measurements) + private static ActivityTagsCollection? MakeTags( + IDictionary? properties, + IDictionary? measurements) { - Dictionary eventMeasurements = new(_commonMeasurements); - if (measurements != null) + if (properties == null && measurements == null) { - foreach (KeyValuePair measurement in measurements) - { - eventMeasurements[measurement.Key] = measurement.Value; - } + 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) + { + if (measurements is null) + { + return _commonMeasurements; + } + if (_commonMeasurements == null) + { + return measurements; + } + + IDictionary eventMeasurements = new Dictionary(_commonMeasurements); + foreach (KeyValuePair measurement in measurements) + { + eventMeasurements[measurement.Key] = measurement.Value; } return eventMeasurements; } - private Dictionary GetEventProperties(IDictionary properties) + private IDictionary? GetEventProperties(IDictionary? properties) { + if (properties is null) + { + return _commonProperties; + } + if (_commonProperties == null) + { + return properties; + } + var eventProperties = new Dictionary(_commonProperties); if (properties != null) { diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index 88b2c2152100..9f58d488a12c 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Frozen; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; @@ -52,7 +53,7 @@ internal class TelemetryCommonProperties( private const string MachineIdCacheKey = "MachineId"; private const string IsDockerContainerCacheKey = "IsDockerContainer"; - public Dictionary GetTelemetryCommonProperties() + public FrozenDictionary GetTelemetryCommonProperties() { return new Dictionary { @@ -82,7 +83,7 @@ public Dictionary GetTelemetryCommonProperties() {ProductType, ExternalTelemetryProperties.GetProductType()}, {LibcRelease, ExternalTelemetryProperties.GetLibcRelease()}, {LibcVersion, ExternalTelemetryProperties.GetLibcVersion()} - }; + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } private string GetMachineId() From 35778b0c8fc85b0ba090085f8c987f683868ee9b Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 12 Jun 2025 13:26:17 -0500 Subject: [PATCH 02/19] WIP: rip and tear --- .../InternalReportInstallSuccessCommand.cs | 2 +- .../dotnet/Commands/MSBuild/MSBuildLogger.cs | 11 - src/Cli/dotnet/CommonOptionsFactory.cs | 24 ++ .../Extensions/ParseResultExtensions.cs | 16 +- src/Cli/dotnet/Parser.cs | 33 ++- src/Cli/dotnet/Program.cs | 236 +++++++----------- src/Cli/dotnet/Telemetry/Telemetry.cs | 3 +- .../MSBuild/DotnetMsbuildInProcTests.cs | 2 +- ...netFirstTimeUseConfigurerWIthStateSetup.cs | 3 +- test/dotnet.Tests/TelemetryCommandTest.cs | 8 +- 10 files changed, 160 insertions(+), 178 deletions(-) diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs index f7465642ef34..4eabcaedadf5 100644 --- a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs @@ -49,7 +49,7 @@ internal ThreadBlockingTelemetry() { var sessionId = Environment.GetEnvironmentVariable(TelemetrySessionIdEnvironmentVariableName); - telemetry = new Telemetry.Telemetry(new NoOpFirstTimeUseNoticeSentinel(), sessionId, blockThreadInitialization: true); + telemetry = new Telemetry.Telemetry(sessionId, blockThreadInitialization: true); } public bool Enabled => telemetry.Enabled; diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs index 8e10b9393474..5c14c52d6006 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs @@ -13,8 +13,6 @@ namespace Microsoft.DotNet.Cli.Commands.MSBuild; public sealed class MSBuildLogger : INodeLogger { - private readonly IFirstTimeUseNoticeSentinel _sentinel = - new FirstTimeUseNoticeSentinel(); private readonly ITelemetry _telemetry; internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval"; @@ -64,7 +62,6 @@ public MSBuildLogger() // 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); } @@ -215,14 +212,6 @@ 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/CommonOptionsFactory.cs b/src/Cli/dotnet/CommonOptionsFactory.cs index f776cd47712a..848255d4b72d 100644 --- a/src/Cli/dotnet/CommonOptionsFactory.cs +++ b/src/Cli/dotnet/CommonOptionsFactory.cs @@ -4,6 +4,9 @@ #nullable disable using System.CommandLine; +using System.CommandLine.Parsing; +using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli; @@ -21,4 +24,25 @@ internal static class CommonOptionsFactory Recursive = recursive, Arity = ArgumentArity.Zero }; + + internal class SetDiagnosticModeAction(Option diagnosticOption) : System.CommandLine.Invocation.SynchronousCommandLineAction + { + public override int Invoke(ParseResult parseResult) + { + if (parseResult.IsDotnetBuiltInCommand()) + { + var diagIsChildOfRoot = parseResult.RootCommandResult.Children.FirstOrDefault((s) => s is OptionResult opt && opt.Option == diagnosticOption) is not null; + + // 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 (diagIsChildOfRoot) + { + Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString); + CommandLoggingContext.SetVerbose(true); + Reporter.Reset(); + } + } + return 0; + } + } } diff --git a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs index 781140230adb..8eaa01e02825 100644 --- a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs +++ b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs @@ -84,9 +84,12 @@ static bool ErrorContainsAllParts(ReadOnlySpan error, string[] parts) public static string RootSubCommandResult(this ParseResult parseResult) { - return parseResult.RootCommandResult.Children? - .Select(child => GetSymbolResultValue(parseResult, child)) - .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty; + CommandResult commandResult = parseResult.CommandResult; + while (commandResult != parseResult.RootCommandResult && commandResult.Parent is CommandResult parentCommand) + { + commandResult = parentCommand; + } + return commandResult.Command.Name; } public static bool IsDotnetBuiltInCommand(this ParseResult parseResult) @@ -100,12 +103,7 @@ 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) - { - return GetBuiltInCommand(parseResult.RootSubCommandResult()) != null || - parseResult.Tokens.Any(token => token.Type == TokenType.Directive) || - (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(DotnetSubCommand))); - } + public static bool CanBeInvoked(this ParseResult parseResult) => parseResult.Action is not null; public static int HandleMissingCommand(this ParseResult parseResult) { diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs index 260c7dd3b82c..b356bc20a083 100644 --- a/src/Cli/dotnet/Parser.cs +++ b/src/Cli/dotnet/Parser.cs @@ -101,14 +101,43 @@ public static class Parser public static readonly Option VersionOption = new("--version") { - Arity = ArgumentArity.Zero + Arity = ArgumentArity.Zero, + Action = new PrintVersionAction() }; + internal class PrintVersionAction : System.CommandLine.Invocation.SynchronousCommandLineAction + { + public PrintVersionAction() + { + Terminating = true; + } + public override int Invoke(ParseResult parseResult) + { + CommandLineInfo.PrintVersion(); + return 0; + } + } + public static readonly Option InfoOption = new("--info") { - Arity = ArgumentArity.Zero + Arity = ArgumentArity.Zero, + Action = new PrintInfoAction() }; + internal class PrintInfoAction : System.CommandLine.Invocation.SynchronousCommandLineAction + { + public PrintInfoAction() + { + Terminating = true; + } + + public override int Invoke(ParseResult parseResult) + { + CommandLineInfo.PrintInfo(); + return 0; + } + } + public static readonly Option ListSdksOption = new("--list-sdks") { Arity = ArgumentArity.Zero diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index fa3fc09973f0..edee3cadc046 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -22,6 +22,11 @@ namespace Microsoft.DotNet.Cli; +public static class Activities +{ + public static ActivitySource s_source = new("dotnet-cli", Product.Version); +} + public class Program { private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; @@ -29,7 +34,10 @@ public class Program public static ITelemetry TelemetryClient; public static int Main(string[] args) { - using AutomaticEncodingRestorer _ = new(); + // capture the time to we can compute muxer/host startup overhead + DateTime mainTimeStamp = DateTime.Now; + using var _mainActivity = Activities.s_source.StartActivity("main"); + using AutomaticEncodingRestorer _encodingRestorer = new(); // Setting output encoding is not available on those platforms if (UILanguageOverride.OperatingSystemSupportsUtf8()) @@ -39,46 +47,17 @@ public static int Main(string[] args) DebugHelper.HandleDebugSwitch(ref args); - // Capture the current timestamp to calculate the host overhead. - DateTime mainTimeStamp = DateTime.Now; - TimeSpan startupTime = mainTimeStamp - Process.GetCurrentProcess().StartTime; + TrackHostStartup(mainTimeStamp); - bool perfLogEnabled = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_PERF_LOG", false); + SetupMSBuildEnvironmentInvariants(); - if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD"))) - { - Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1"); - } - - // Avoid create temp directory with root permission and later prevent access in non sudo - if (SudoEnvironmentDirectoryOverride.IsRunningUnderSudo()) - { - perfLogEnabled = false; - } - - PerformanceLogStartupInformation startupInfo = null; - if (perfLogEnabled) - { - startupInfo = new PerformanceLogStartupInformation(mainTimeStamp); - PerformanceLogManager.InitializeAndStartCleanup(FileSystemWrapper.Default); - } - - PerformanceLogEventListener perLogEventListener = null; try { - if (perfLogEnabled) - { - perLogEventListener = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory); - } - - PerformanceLogEventSource.Log.LogStartUpInformation(startupInfo); - PerformanceLogEventSource.Log.CLIStart(); - InitializeProcess(); try { - return ProcessArgs(args, startupTime); + return ProcessArgs(args); } catch (Exception e) when (e.ShouldBeDisplayedAsError()) { @@ -109,25 +88,35 @@ public static int Main(string[] args) } finally { - if (perLogEventListener != null) - { - perLogEventListener.Dispose(); - } + Activities.s_source.Dispose(); } } - internal static int ProcessArgs(string[] args) + private static void TrackHostStartup(DateTime mainTimeStamp) { - return ProcessArgs(args, new TimeSpan(0)); + var hostStartupActivity = Activities.s_source.CreateActivity("host-startup", ActivityKind.Server); + hostStartupActivity.SetStartTime(Process.GetCurrentProcess().StartTime); + hostStartupActivity.SetEndTime(mainTimeStamp); + hostStartupActivity.SetStatus(ActivityStatusCode.Ok); + hostStartupActivity.Dispose(); } - internal static int ProcessArgs(string[] args, TimeSpan startupTime) + /// + /// 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. + /// + private static void SetupMSBuildEnvironmentInvariants() { - Dictionary performanceData = []; + if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD"))) + { + Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1"); + } + } - PerformanceLogEventSource.Log.BuiltInCommandParserStart(); + internal static int ProcessArgs(string[] args) + { ParseResult parseResult; - using (new PerformanceMeasurement(performanceData, "Parse Time")) + using (var _parseActivity = Activities.s_source.StartActivity("parse")) { parseResult = Parser.Instance.Parse(args); @@ -136,102 +125,70 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) // https://github.com/dotnet/sdk/issues/20195 SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult); } - PerformanceLogEventSource.Log.BuiltInCommandParserStop(); - using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel()) + using (var _firstTimeUseActivity = Activities.s_source.StartActivity("first-time-use")) { - 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); + IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel(); - PerformanceLogEventSource.Log.TelemetryRegistrationStop(); - - if (parseResult.GetValue(Parser.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.VersionOption) && parseResult.IsTopLevelDotnetCommand()) - { - CommandLineInfo.PrintVersion(); - return 0; - } - else if (parseResult.HasOption(Parser.InfoOption) && parseResult.IsTopLevelDotnetCommand()) + IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel(); + IFileSentinel toolPathSentinel = new FileSentinel( + new FilePath( + Path.Combine( + CliFolderPathCalculator.DotnetUserProfileFolderPath, + ToolPathSentinelFileName))); + + 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.Name.Equals(Parser.InstallSuccessCommand.Name)) { - CommandLineInfo.PrintInfo(); - return 0; + aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel(); + firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel(); + toolPathSentinel = new NoOpFileSentinel(exists: false); + isDotnetBeingInvokedFromNativeInstaller = true; } - 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.Name.Equals(Parser.InstallSuccessCommand.Name)) - { - 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(); - } + 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, + skipFirstTimeUseCheck: getStarOptionPassed); } + var telemetryClient = new Telemetry.Telemetry(); + TelemetryEventEntry.Subscribe(telemetryClient.TrackEvent); + TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + if (CommandLoggingContext.IsVerbose) { Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}"); } - PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart(); - performanceData.Add("Startup Time", startupTime.TotalMilliseconds); - TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData)); - PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop(); int exitCode; if (parseResult.CanBeInvoked()) @@ -240,15 +197,16 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) } else { - PerformanceLogEventSource.Log.ExtensibleCommandResolverStart(); try { + var _lookupExternalCommandActivity = Activities.s_source.StartActivity("lookup-external-command"); string commandName = "dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand); var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec( new DefaultCommandResolverPolicy(), commandName, args.GetSubArguments(), FrameworkConstants.CommonFrameworks.NetStandardApp15); + _lookupExternalCommandActivity?.Dispose(); if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode) { @@ -256,13 +214,10 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) } else { + var _executionActivity = Activities.s_source.StartActivity("execute-extensible-command"); var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec); - PerformanceLogEventSource.Log.ExtensibleCommandResolverStop(); - - PerformanceLogEventSource.Log.ExtensibleCommandStart(); var result = resolvedCommand.Execute(); - PerformanceLogEventSource.Log.ExtensibleCommandStop(); - + _executionActivity?.Dispose(); exitCode = result.ExitCode; } } @@ -274,12 +229,7 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) } } - PerformanceLogEventSource.Log.TelemetryClientFlushStart(); - TelemetryClient.Flush(); - PerformanceLogEventSource.Log.TelemetryClientFlushStop(); - - TelemetryClient.Dispose(); - + telemetryClient.Dispose(); return exitCode; static int? TryRunFileBasedApp(ParseResult parseResult) @@ -310,9 +260,7 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode) { Debug.Assert(parseResult.CanBeInvoked()); - - PerformanceLogEventSource.Log.BuiltInCommandStart(); - + using var _invocationActivity = Activities.s_source.StartActivity("invocation"); try { exitCode = parseResult.Invoke(); @@ -322,8 +270,6 @@ static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode) { exitCode = Parser.ExceptionHandler(exception, parseResult); } - - PerformanceLogEventSource.Log.BuiltInCommandStop(); } } @@ -371,7 +317,6 @@ private static void ConfigureDotNetForFirstTimeUse( bool isDotnetBeingInvokedFromNativeInstaller, DotnetFirstRunConfiguration dotnetFirstRunConfiguration, IEnvironmentProvider environmentProvider, - Dictionary performanceMeasurements, bool skipFirstTimeUseCheck) { var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck; @@ -387,7 +332,6 @@ private static void ConfigureDotNetForFirstTimeUse( dotnetFirstRunConfiguration, reporter, environmentPath, - performanceMeasurements, skipFirstTimeUseCheck: skipFirstTimeUseCheck); dotnetConfigurer.Configure(); diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs index f908d01e7fe0..4f05657fbe50 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -44,8 +44,7 @@ public Telemetry( environmentProvider ??= new EnvironmentProvider(); - Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault) - && PermissionExists(sentinel); + Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); if (!Enabled) { diff --git a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs index a2a26316e3d8..1ea353b3443b 100644 --- a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs +++ b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs @@ -54,7 +54,7 @@ private string[] GetArgsForMSBuild(Func sentinelExists, out Telemetry.Tele { Telemetry.Telemetry.DisableForTests(); // reset static session id modified by telemetry constructor - telemetry = new Telemetry.Telemetry(new MockFirstTimeUseNoticeSentinel(sentinelExists)); + telemetry = new Telemetry.Telemetry(); MSBuildForwardingApp msBuildForwardingApp = new(Enumerable.Empty()); diff --git a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs index d694240d865b..6d3075efaa01 100644 --- a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs +++ b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs @@ -252,8 +252,7 @@ private Telemetry RunConfigUsingMocks(bool isInstallerRun) configurer.Configure(); - return new Telemetry(firstTimeUseNoticeSentinel, - "test", + return new Telemetry("test", environmentProvider: _environmentProviderObject, senderCount: 0); } diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs index bda1eaf6e3b2..7414c235a6dd 100644 --- a/test/dotnet.Tests/TelemetryCommandTest.cs +++ b/test/dotnet.Tests/TelemetryCommandTest.cs @@ -56,7 +56,7 @@ public void TopLevelCommandNameShouldBeSentToTelemetry() public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData() { string[] args = { "help" }; - Cli.Program.ProcessArgs(args, new TimeSpan(12345)); + Cli.Program.ProcessArgs(args); _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && e.Properties.ContainsKey("verb") && @@ -85,7 +85,7 @@ public void TopLevelCommandNameShouldBeSentToTelemetryWithoutStartupTime() public void TopLevelCommandNameShouldBeSentToTelemetryZeroStartupTime() { string[] args = { "help" }; - Cli.Program.ProcessArgs(args, new TimeSpan(0)); + Cli.Program.ProcessArgs(args); _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && e.Properties.ContainsKey("verb") && @@ -115,7 +115,7 @@ public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetryWithPerformanceD { const string argumentToSend = "console"; string[] args = { "new", argumentToSend }; - Cli.Program.ProcessArgs(args, new TimeSpan(23456)); + Cli.Program.ProcessArgs(args); _fakeTelemetry .LogEntries.Should() .Contain(e => e.EventName == "sublevelparser/command" && @@ -272,7 +272,7 @@ public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetryWithPerforman const string optionKey = "verbosity"; const string optionValueToSend = "minimal"; string[] args = { "restore", "--" + optionKey, optionValueToSend }; - Cli.Program.ProcessArgs(args, new TimeSpan(34567)); + Cli.Program.ProcessArgs(args); _fakeTelemetry .LogEntries.Should() .Contain(e => e.EventName == "sublevelparser/command" && From 53fdb6ea9ccc522fa6ae8205347e5eadafa36627 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 12 Jun 2025 17:09:05 -0500 Subject: [PATCH 03/19] WIP: tool install --- Directory.Packages.props | 6 +- src/Cli/dotnet/Activities.cs | 10 +++ .../ToolInstallGlobalOrToolPathCommand.cs | 16 +++-- src/Cli/dotnet/Program.cs | 61 +++++++++++++++---- .../ToolPackage/ToolPackageDownloader.cs | 11 +++- .../ToolPackage/ToolPackageDownloaderBase.cs | 3 + src/Cli/dotnet/dotnet.csproj | 3 + 7 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 src/Cli/dotnet/Activities.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index abe2e9e23b4c..c8b530b9d628 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -95,6 +95,9 @@ + + + @@ -127,7 +130,6 @@ -