diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs index 9f4665572436..0742764a8b7c 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs @@ -6,15 +6,27 @@ namespace Microsoft.DotNet.Cli.Utils; /// -/// Contains helpers for working with Activities in the .NET CLI. +/// 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); + + /// + /// The environment variable used to transfer the chain of parent activity IDs. + /// This should be used when constructing new sub-processes in order to + /// track spans across calls. + /// + public const string TRACEPARENT = nameof(TRACEPARENT); + /// + /// The environment variable used to transfer the trace state of the parent activities. + /// This should be used when constructing new sub-processes in order to + /// track spans across calls. + /// + public const string TRACESTATE = nameof(TRACESTATE); } diff --git a/src/Cli/dotnet/CommonOptionsFactory.cs b/src/Cli/dotnet/CommonOptionsFactory.cs index f776cd47712a..fa9dc531eb83 100644 --- a/src/Cli/dotnet/CommonOptionsFactory.cs +++ b/src/Cli/dotnet/CommonOptionsFactory.cs @@ -1,9 +1,9 @@ // 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.Invocation; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli; @@ -19,6 +19,22 @@ internal static class CommonOptionsFactory { Description = CliStrings.SDKDiagnosticsCommandDefinition, Recursive = recursive, - Arity = ArgumentArity.Zero + Arity = ArgumentArity.Zero, + Action = new SetDiagnosticModeAction() }; + + /// + /// Sets a few verbose diagnostics flags across the CLI. + /// Other commands may also use this to set their verbosity flags to a higher value or similar behaviors. + /// + internal class SetDiagnosticModeAction() : SynchronousCommandLineAction + { + public override int Invoke(ParseResult parseResult) + { + 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 17d74908d42e..242bda2fe7d8 100644 --- a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs +++ b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs @@ -15,17 +15,17 @@ 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 @@ -56,14 +56,17 @@ public static void ShowHelpOrErrorIfAppropriate(this 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 + /// + /// 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) @@ -85,9 +88,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) @@ -101,12 +107,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 Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null || - parseResult.Tokens.Any(token => token.Type == TokenType.Directive) || - (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.DotnetSubCommand))); - } + public static bool CanBeInvoked(this ParseResult parseResult) => parseResult.Action is not null; public static int HandleMissingCommand(this ParseResult parseResult) { @@ -163,34 +164,14 @@ public static bool DiagOptionPrecedesSubcommand(this string[] args, string subCo return false; } - private static string? GetSymbolResultValue(ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch - { - CommandResult commandResult => commandResult.Command.Name, - ArgumentResult argResult => argResult.Tokens.FirstOrDefault()?.Value, - _ => parseResult.GetResult(Parser.DotnetSubCommand)?.GetValueOrDefault() - }; - public static bool BothArchAndOsOptionsSpecified(this ParseResult parseResult) => (parseResult.HasOption(CommonOptions.ArchitectureOption) || parseResult.HasOption(CommonOptions.LongFormArchitectureOption)) && parseResult.HasOption(CommonOptions.OperatingSystemOption); - internal static string? GetCommandLineRuntimeIdentifier(this ParseResult parseResult) - { - return parseResult.HasOption(CommonOptions.RuntimeOptionName) ? - parseResult.GetValue(CommonOptions.RuntimeOptionName) : - parseResult.HasOption(CommonOptions.OperatingSystemOption) || - parseResult.HasOption(CommonOptions.ArchitectureOption) || - parseResult.HasOption(CommonOptions.LongFormArchitectureOption) ? - CommonOptions.ResolveRidShorthandOptionsToRuntimeIdentifier( - parseResult.GetValue(CommonOptions.OperatingSystemOption), - CommonOptions.ArchOptionValue(parseResult)) : - null; - } - public static bool UsingRunCommandShorthandProjectOption(this ParseResult parseResult) { - if (parseResult.HasOption(RunCommandParser.PropertyOption) && parseResult.GetValue(RunCommandParser.PropertyOption)!.Any()) + if (parseResult.HasOption(RunCommandParser.PropertyOption) && (parseResult.GetValue(RunCommandParser.PropertyOption)?.Any() ?? false)) { var projVals = parseResult.GetRunCommandShorthandProjectValues(); if (projVals?.Any() is true) diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs index 4bddb07f976e..bb9c9e236f48 100644 --- a/src/Cli/dotnet/Parser.cs +++ b/src/Cli/dotnet/Parser.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.CommandLine.Completions; using System.CommandLine.Invocation; @@ -96,14 +94,44 @@ public static class Parser public static readonly Option VersionOption = new("--version") { - Arity = ArgumentArity.Zero + Arity = ArgumentArity.Zero, + Action = new PrintVersionAction() }; + internal class PrintVersionAction : 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 : 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 @@ -169,31 +197,22 @@ private static RootCommand ConfigureCommandLine(RootCommand rootCommand) rootCommand.SetAction(parseResult => { - if (parseResult.GetValue(DiagOption) && parseResult.Tokens.Count == 1) - { - // 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 - parseResult.InvocationConfiguration.Output.WriteLine(CliUsage.HelpText); - return 0; - } + // when user does not specify any args (just "dotnet"), a usage needs to be printed + parseResult.InvocationConfiguration.Output.WriteLine(CliUsage.HelpText); + return 0; }); return rootCommand; } - public static Command GetBuiltInCommand(string commandName) => + public static Command? GetBuiltInCommand(string commandName) => 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)) @@ -256,8 +275,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) { @@ -277,13 +295,13 @@ internal static int ExceptionHandler(Exception exception, ParseResult parseResul exception.Message.Red().Bold()); parseResult.ShowHelp(); } - else if (exception.GetType().Name.Equals("WorkloadManifestCompositionException")) + else if (exception is not null && exception.GetType().Name.Equals("WorkloadManifestCompositionException")) { Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ? exception.ToString().Red().Bold() : exception.Message.Red().Bold()); } - else + else if (exception is not null) { Reporter.Error.Write("Unhandled exception: ".Red().Bold()); Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ? @@ -371,7 +389,7 @@ public override void Write(HelpContext context) } else if (command.Name.Equals(FormatCommandParser.GetCommand().Name)) { - var arguments = context.ParseResult.GetValue(FormatCommandParser.Arguments); + var arguments = context.ParseResult.GetValue(FormatCommandParser.Arguments) ?? []; new FormatForwardingApp([.. arguments, .. helpArgs]).Execute(); } else if (command.Name.Equals(FsiCommandParser.GetCommand().Name)) @@ -398,9 +416,9 @@ public override void Write(HelpContext context) { if (command.Name.Equals(ListReferenceCommandParser.GetCommand().Name)) { - Command listCommand = command.Parents.Single() as Command; + 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) { @@ -431,6 +449,7 @@ internal PrintCliSchemaAction() { Terminating = true; } + public override int Invoke(ParseResult parseResult) { CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.InvocationConfiguration.Output, Program.TelemetryClient); diff --git a/src/Cli/dotnet/PerformanceLogEventSource.cs b/src/Cli/dotnet/PerformanceLogEventSource.cs index 0879b02f1725..2957abedd93b 100644 --- a/src/Cli/dotnet/PerformanceLogEventSource.cs +++ b/src/Cli/dotnet/PerformanceLogEventSource.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Diagnostics; using System.Diagnostics.Tracing; using System.Reflection; @@ -21,7 +19,7 @@ private PerformanceLogEventSource() } [NonEvent] - internal void LogStartUpInformation(PerformanceLogStartupInformation startupInfo) + internal void LogStartUpInformation(PerformanceLogStartupInformation? startupInfo) { if (!IsEnabled()) { @@ -33,7 +31,7 @@ internal void LogStartUpInformation(PerformanceLogStartupInformation startupInfo LogMachineConfiguration(); OSInfo(RuntimeEnvironment.OperatingSystem, RuntimeEnvironment.OperatingSystemVersion, RuntimeEnvironment.OperatingSystemPlatform.ToString()); - SDKInfo(Product.Version, commitSha, RuntimeInformation.RuntimeIdentifier, versionFile.BuildRid, AppContext.BaseDirectory); + SDKInfo(Product.Version, commitSha, RuntimeInformation.RuntimeIdentifier, versionFile.BuildRid!, AppContext.BaseDirectory); EnvironmentInfo(Environment.CommandLine); LogMemoryConfiguration(); LogDrives(); @@ -44,7 +42,7 @@ internal void LogStartUpInformation(PerformanceLogStartupInformation startupInfo { if (startupInfo.TimedAssembly != null) { - AssemblyLoad(startupInfo.TimedAssembly.GetName().Name, startupInfo.AssemblyLoadTime.TotalMilliseconds); + AssemblyLoad(startupInfo.TimedAssembly.GetName().Name!, startupInfo.AssemblyLoadTime.TotalMilliseconds); } Process currentProcess = Process.GetCurrentProcess(); @@ -312,7 +310,7 @@ public PerformanceLogStartupInformation(DateTime mainTimeStamp) } internal DateTime MainTimeStamp { get; private set; } - internal Assembly TimedAssembly { get; private set; } + internal Assembly? TimedAssembly { get; private set; } internal TimeSpan AssemblyLoadTime { get; private set; } private void MeasureModuleLoad() @@ -323,7 +321,7 @@ private void MeasureModuleLoad() { foreach (Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies()) { - if (loadedAssembly.GetName().Name.Equals(assemblyName)) + if (loadedAssembly.GetName().Name is string n && n.Equals(assemblyName)) { // If the assembly is already loaded, then bail. return; @@ -426,7 +424,7 @@ private void Initialize() { using (StreamReader reader = new(File.OpenRead("/proc/meminfo"))) { - string line; + string? line; while (!Valid && ((line = reader.ReadLine()) != null)) { if (line.StartsWith(MemTotal) || line.StartsWith(MemAvailable)) diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index d88858b67e33..34a6af49ceb6 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; @@ -24,307 +22,348 @@ namespace Microsoft.DotNet.Cli; public class Program { - private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; - - public static ITelemetry TelemetryClient; - public static int Main(string[] args) + private static readonly string s_toolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; + public static ITelemetry TelemetryClient { get; } + internal static PerformanceLogEventListener? performanceLogEventListener; + private static Dictionary performanceData = []; + private static readonly Activity? s_mainActivity; + private static readonly DateTime s_mainTimeStamp; + + static Program() { - using AutomaticEncodingRestorer _ = new(); - - // Setting output encoding is not available on those platforms - if (UILanguageOverride.OperatingSystemSupportsUtf8()) - { - Console.OutputEncoding = Encoding.UTF8; - } - - DebugHelper.HandleDebugSwitch(ref args); - - // Capture the current timestamp to calculate the host overhead. - DateTime mainTimeStamp = DateTime.Now; - TimeSpan startupTime = mainTimeStamp - Process.GetCurrentProcess().StartTime; - + s_mainTimeStamp = DateTime.Now; + s_mainActivity = Activities.Source.CreateActivity("main", kind: ActivityKind.Internal); + s_mainActivity?.SetStartTime(Process.GetCurrentProcess().StartTime); + TrackHostStartup(s_mainTimeStamp); + SetupMSBuildEnvironmentInvariants(); bool perfLogEnabled = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_PERF_LOG", false); - - 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 = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory); + } + else + { + performanceLogEventListener = null; } + TelemetryClient = InitializeTelemetry(); - PerformanceLogEventListener perLogEventListener = null; - try + } + + public static int Main(string[] args) + { + using AutomaticEncodingRestorer _encodingRestorer = new(); + TimeSpan startupTime = s_mainTimeStamp - Process.GetCurrentProcess().StartTime; + performanceData.Add("Startup Time", startupTime.TotalMilliseconds); + // Setting output encoding is not available on those platforms + if (UILanguageOverride.OperatingSystemSupportsUtf8()) { - if (perfLogEnabled) - { - perLogEventListener = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory); - } + Console.OutputEncoding = Encoding.UTF8; + } + DebugHelper.HandleDebugSwitch(ref args); + PerformanceLogStartupInformation? startupInfo = null; + if (performanceLogEventListener != null) + { + startupInfo = new PerformanceLogStartupInformation(s_mainTimeStamp); + } + try + { PerformanceLogEventSource.Log.LogStartUpInformation(startupInfo); PerformanceLogEventSource.Log.CLIStart(); - InitializeProcess(); + return ProcessArgs(args); + } + catch (Exception e) when (e.ShouldBeDisplayedAsError()) + { + Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose + ? e.ToString().Red().Bold() + : e.Message.Red().Bold()); - try + if (e is CommandParsingException commandParsingException && commandParsingException.ParseResult != null) { - return ProcessArgs(args, startupTime); + commandParsingException.ParseResult.ShowHelp(); } - 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; + } + 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(); - } + return 1; } finally { - if (perLogEventListener != null) - { - perLogEventListener.Dispose(); - } + PerformanceLogEventSource.Log.TelemetryClientFlushStart(); + TelemetryClient.Flush(); + PerformanceLogEventSource.Log.TelemetryClientFlushStop(); + PerformanceLogEventSource.Log.CLIStop(); + Shutdown(); } } - internal static int ProcessArgs(string[] args) + public static void Shutdown() { - return ProcessArgs(args, new TimeSpan(0)); + s_mainActivity?.Stop(); + performanceLogEventListener?.Dispose(); + Activities.Source.Dispose(); } - internal static int ProcessArgs(string[] args, TimeSpan startupTime) + private static void TrackHostStartup(DateTime mainTimeStamp) { - Dictionary performanceData = []; + using var hostStartupActivity = Activities.Source.StartActivity("host-startup"); + hostStartupActivity?.SetStartTime(Process.GetCurrentProcess().StartTime); + hostStartupActivity?.SetEndTime(mainTimeStamp); + hostStartupActivity?.SetStatus(ActivityStatusCode.Ok); + } - PerformanceLogEventSource.Log.BuiltInCommandParserStart(); - ParseResult parseResult; - using (new PerformanceMeasurement(performanceData, "Parse Time")) + /// + /// 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() + { + if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD"))) { - parseResult = Parser.Parse(args); + Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1"); + } + } - // 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); + private static string GetCommandName(ParseResult r) + { + if (r.Action is Parser.PrintVersionAction) + { + // If the action is PrintVersionAction, we return the command name as "dotnet --version" + return "dotnet --version"; + } + else if (r.Action is Parser.PrintInfoAction) + { + // If the action is PrintHelpAction, we return the command name as "dotnet --help" + return "dotnet --info"; } - PerformanceLogEventSource.Log.BuiltInCommandParserStop(); - using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel()) + // Walk the parent command tree to find the top-level command name and get the full command name for this parseresult. + List parentNames = [r.CommandResult.Command.Name]; + var current = r.CommandResult.Parent; + while (current is CommandResult parentCommandResult) { - IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel; - IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel(); - IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, ToolPathSentinelFileName))); + parentNames.Add(parentCommandResult.Command.Name); + current = parentCommandResult.Parent; + } + parentNames.Reverse(); + return string.Join(' ', parentNames); + } - PerformanceLogEventSource.Log.TelemetryRegistrationStart(); + private static void SetDisplayName(Activity? activity, ParseResult parseResult) + { + if (activity == null) + { + return; + } + var name = GetCommandName(parseResult); - TelemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel); - TelemetryEventEntry.Subscribe(TelemetryClient.TrackEvent); - TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + // Set the display name to the full command name + activity.DisplayName = name; - PerformanceLogEventSource.Log.TelemetryRegistrationStop(); + // Set the command name as an attribute for better filtering in telemetry + activity.SetTag("command.name", name); + } - 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()) + internal static int ProcessArgs(string[] args) + { + ParseResult parseResult = ParseArgs(args); + SetupDotnetFirstRun(parseResult); + + if (parseResult.CanBeInvoked()) + { + PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart(); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData)); + PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop(); + InvokeBuiltInCommand(parseResult, out var exitCode); + return exitCode; + } + else + { + try { - CommandLineInfo.PrintInfo(); - return 0; + return LookupAndExecuteCommand(args, parseResult); } - else + catch (CommandUnknownException e) { - 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(); + Reporter.Error.WriteLine(e.Message.Red()); + Reporter.Output.WriteLine(e.InstructionMessage); + return 1; } } + } - if (CommandLoggingContext.IsVerbose) + private static int LookupAndExecuteCommand(string[] args, ParseResult parseResult) + { + var lookupExternalCommandActivity = Activities.Source.StartActivity("lookup-external-command"); + PerformanceLogEventSource.Log.ExtensibleCommandResolverStart(); + 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) { - Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}"); + lookupExternalCommandActivity?.Dispose(); + return fileBasedAppExitCode; } + else + { + var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec); + PerformanceLogEventSource.Log.ExtensibleCommandResolverStop(); + lookupExternalCommandActivity?.Dispose(); + + PerformanceLogEventSource.Log.ExtensibleCommandStart(); + using var _executionActivity = Activities.Source.StartActivity("execute-extensible-command"); + var result = resolvedCommand.Execute(); + PerformanceLogEventSource.Log.ExtensibleCommandStop(); + return result.ExitCode; + } + } + + private static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode) + { + Debug.Assert(parseResult.CanBeInvoked()); + using var _invocationActivity = Activities.Source.StartActivity("invocation"); PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart(); - performanceData.Add("Startup Time", startupTime.TotalMilliseconds); TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData)); PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop(); - - int exitCode; - if (parseResult.CanBeInvoked()) + try { - InvokeBuiltInCommand(parseResult, out exitCode); + exitCode = parseResult.Invoke(); + exitCode = AdjustExitCode(parseResult, exitCode); } - else + catch (Exception exception) { - PerformanceLogEventSource.Log.ExtensibleCommandResolverStart(); - try + exitCode = Parser.ExceptionHandler(exception, parseResult); + } + } + + private static int? TryRunFileBasedApp(ParseResult parseResult) + { + // 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.CommandResult.Command is RootCommand + && parseResult.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile + && VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile)) + { + List otherTokens = new(parseResult.Tokens.Count - 1); + foreach (var token in parseResult.Tokens) { - string commandName = "dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand); - var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec( - new DefaultCommandResolverPolicy(), - commandName, - args.GetSubArguments(), - FrameworkConstants.CommonFrameworks.NetStandardApp15); - - if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode) + if (token.Type != TokenType.Argument || token.Value != unmatchedCommandOrFile) { - exitCode = fileBasedAppExitCode; - } - else - { - var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec); - PerformanceLogEventSource.Log.ExtensibleCommandResolverStop(); - - PerformanceLogEventSource.Log.ExtensibleCommandStart(); - var result = resolvedCommand.Execute(); - PerformanceLogEventSource.Log.ExtensibleCommandStop(); - - exitCode = result.ExitCode; + otherTokens.Add(token.Value); } } - catch (CommandUnknownException e) - { - Reporter.Error.WriteLine(e.Message.Red()); - Reporter.Output.WriteLine(e.InstructionMessage); - exitCode = 1; - } - } + parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile, .. otherTokens]); - PerformanceLogEventSource.Log.TelemetryClientFlushStart(); - TelemetryClient.Flush(); - PerformanceLogEventSource.Log.TelemetryClientFlushStop(); + InvokeBuiltInCommand(parseResult, out var exitCode); + return exitCode; + } - TelemetryClient.Dispose(); + return null; + } - return exitCode; + private static ITelemetry InitializeTelemetry() + { + PerformanceLogEventSource.Log.TelemetryRegistrationStart(); + var telemetryClient = new Telemetry.Telemetry(); + TelemetryEventEntry.Subscribe(telemetryClient.TrackEvent); + TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); + PerformanceLogEventSource.Log.TelemetryRegistrationStop(); - static int? TryRunFileBasedApp(ParseResult parseResult) + if (CommandLoggingContext.IsVerbose) { - // 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.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile - && VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile)) - { - List otherTokens = new(parseResult.Tokens.Count - 1); - foreach (var token in parseResult.Tokens) - { - if (token.Type != TokenType.Argument || token.Value != unmatchedCommandOrFile) - { - otherTokens.Add(token.Value); - } - } + Console.WriteLine($"Telemetry is: {(telemetryClient.Enabled ? "Enabled" : "Disabled")}"); + } - parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile, .. otherTokens]); + return telemetryClient; + } - InvokeBuiltInCommand(parseResult, out var exitCode); - return exitCode; - } + private static ParseResult ParseArgs(string[] args) + { + ParseResult parseResult; + using (new PerformanceMeasurement(performanceData, "Parse Time")) + using (var _parseActivity = Activities.Source.StartActivity("parse")) + { + parseResult = Parser.Parse(args); - return null; + // 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(); + SetDisplayName(s_mainActivity, parseResult); + return parseResult; + } - static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode) + private static void SetupDotnetFirstRun(ParseResult parseResult) + { + PerformanceLogEventSource.Log.FirstTimeConfigurationStart(); + 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()); + + ReportDotnetHomeUsage(environmentProvider); + + var isDotnetBeingInvokedFromNativeInstaller = false; + if (parseResult.CommandResult.Command.Name.Equals(Parser.InstallSuccessCommand.Name)) { - Debug.Assert(parseResult.CanBeInvoked()); + aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel(); + firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel(); + toolPathSentinel = new NoOpFileSentinel(exists: false); + isDotnetBeingInvokedFromNativeInstaller = true; + } - PerformanceLogEventSource.Log.BuiltInCommandStart(); + var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration( + generateAspNetCertificate: generateAspNetCertificate, + telemetryOptout: telemetryOptout, + addGlobalToolsToPath: addGlobalToolsToPath, + nologo: nologo, + skipWorkloadIntegrityCheck: skipWorkloadIntegrityCheck); - try - { - exitCode = Parser.Invoke(parseResult); - exitCode = AdjustExitCode(parseResult, exitCode); - } - catch (Exception exception) - { - exitCode = Parser.ExceptionHandler(exception, parseResult); - } + 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)))); - PerformanceLogEventSource.Log.BuiltInCommandStop(); - } + ConfigureDotNetForFirstTimeUse( + firstTimeUseNoticeSentinel, + aspNetCertificateSentinel, + toolPathSentinel, + isDotnetBeingInvokedFromNativeInstaller, + dotnetFirstRunConfiguration, + environmentProvider, + skipFirstTimeUseCheck: getStarOptionPassed); + PerformanceLogEventSource.Log.FirstTimeConfigurationStop(); } private static int AdjustExitCode(ParseResult parseResult, int exitCode) @@ -357,11 +396,7 @@ private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) return; } - Reporter.Verbose.WriteLine( - string.Format( - LocalizableStrings.DotnetCliHomeUsed, - home, - CliFolderPathCalculator.DotnetHomeVariableName)); + Reporter.Verbose.WriteLine(string.Format(LocalizableStrings.DotnetCliHomeUsed, home, CliFolderPathCalculator.DotnetHomeVariableName)); } private static void ConfigureDotNetForFirstTimeUse( @@ -371,7 +406,6 @@ private static void ConfigureDotNetForFirstTimeUse( bool isDotnetBeingInvokedFromNativeInstaller, DotnetFirstRunConfiguration dotnetFirstRunConfiguration, IEnvironmentProvider environmentProvider, - Dictionary performanceMeasurements, bool skipFirstTimeUseCheck) { var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck; @@ -387,7 +421,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 ba190b169500..5807ec380c3d 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -17,7 +17,7 @@ public class Telemetry : ITelemetry internal static bool DisabledForTests = false; private readonly int _senderCount; private TelemetryClient? _client = null; - private FrozenDictionary? _commonProperties = null; + private FrozenDictionary? _commonProperties = null; private FrozenDictionary? _commonMeasurements = null; private Task? _trackEventTask = null; @@ -87,26 +87,31 @@ private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel) return sentinel.Exists(); } - public void TrackEvent(string eventName, IDictionary properties, - IDictionary measurements) + public void TrackEvent(string? eventName, IDictionary? properties, + IDictionary? measurements) { if (!Enabled) { return; } - //continue the task in different threads - if (_trackEventTask == null) + if (eventName is null) { - _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements)); return; } - else - { - _trackEventTask = _trackEventTask.ContinueWith( - x => TrackEventTask(eventName, properties, measurements) - ); - } + + //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() @@ -129,12 +134,16 @@ public void Dispose() } } - public void ThreadBlockingTrackEvent(string eventName, IDictionary properties, IDictionary measurements) + public void ThreadBlockingTrackEvent(string? eventName, IDictionary? properties, IDictionary? measurements) { if (!Enabled) { return; } + if (eventName is null) + { + return; + } TrackEventTask(eventName, properties, measurements); } @@ -167,8 +176,8 @@ private void InitializeTelemetry() private void TrackEventTask( string eventName, - IDictionary properties, - IDictionary measurements) + IDictionary? properties, + IDictionary? measurements) { if (_client == null) { @@ -180,7 +189,7 @@ private void TrackEventTask( var eventProperties = GetEventProperties(properties); var eventMeasurements = GetEventMeasures(measurements); - eventProperties ??= new Dictionary(); + eventProperties ??= new Dictionary(); eventProperties.Add("event id", Guid.NewGuid().ToString()); _client.TrackEvent(PrependProducerNamespace(eventName), eventProperties, eventMeasurements); @@ -194,7 +203,7 @@ private void TrackEventTask( private static ActivityEvent CreateActivityEvent( string eventName, - IDictionary? properties, + IDictionary? properties, IDictionary? measurements) { var tags = MakeTags(properties, measurements); @@ -204,7 +213,7 @@ private static ActivityEvent CreateActivityEvent( } private static ActivityTagsCollection? MakeTags( - IDictionary? properties, + IDictionary? properties, IDictionary? measurements) { if (properties == null && measurements == null) @@ -236,12 +245,12 @@ private static ActivityEvent CreateActivityEvent( }; } - private IDictionary? GetEventProperties(IDictionary? properties) + private IDictionary? GetEventProperties(IDictionary? properties) { return (properties, _commonProperties) switch { (null, null) => null, - (null, not null) => _commonProperties == FrozenDictionary.Empty ? null : new Dictionary(_commonProperties), + (null, not null) => _commonProperties == FrozenDictionary.Empty ? null : new Dictionary(_commonProperties), (not null, null) => properties, (not null, not null) => Combine(_commonProperties, properties), }; diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs index bda1eaf6e3b2..42ab6e53d0e3 100644 --- a/test/dotnet.Tests/TelemetryCommandTest.cs +++ b/test/dotnet.Tests/TelemetryCommandTest.cs @@ -56,15 +56,11 @@ 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") && - 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); + e.Properties["verb"] == Sha256Hasher.Hash("HELP")); } [Fact] @@ -75,24 +71,7 @@ public void TopLevelCommandNameShouldBeSentToTelemetryWithoutStartupTime() _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); + e.Properties["verb"] == Sha256Hasher.Hash("HELP")); } [Fact] @@ -110,25 +89,6 @@ public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetry() 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() { @@ -266,26 +226,6 @@ public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetry() 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() {