diff --git a/NuGet.config b/NuGet.config index d34e6bd81a..d060e04ff3 100644 --- a/NuGet.config +++ b/NuGet.config @@ -15,8 +15,8 @@ - - + + diff --git a/eng/Versions.props b/eng/Versions.props index 3e9c94afa7..ce4e543109 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -45,8 +45,7 @@ 6.0.0 5.0.1 - 2.0.0-beta1.20468.1 - 2.0.0-beta1.20074.1 + 2.0.0-beta4.25072.1 5.0.0 4.5.1 4.5.5 diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs index 4f37f455e0..abf259de40 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs @@ -5,11 +5,10 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.CommandLine; -using System.CommandLine.Builder; using System.CommandLine.Help; using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.CommandLine.Parsing; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -67,7 +66,7 @@ public IReadOnlyList ExecuteAndCapture(string commandLine, IServiceProvi /// other errors public void Execute(string commandLine, IServiceProvider services) { - string[] commandLineArray = CommandLineStringSplitter.Instance.Split(commandLine).ToArray(); + string[] commandLineArray = CommandLineParser.SplitCommandLine(commandLine).ToArray(); if (commandLineArray.Length <= 0) { throw new ArgumentException("Empty command line", nameof(commandLine)); @@ -89,7 +88,7 @@ public void Execute(string commandLine, IServiceProvider services) public void Execute(string commandName, string commandArguments, IServiceProvider services) { commandName = commandName.Trim(); - string[] commandLineArray = CommandLineStringSplitter.Instance.Split(commandName + " " + (commandArguments ?? "")).ToArray(); + string[] commandLineArray = CommandLineParser.SplitCommandLine(commandName + " " + (commandArguments ?? "")).ToArray(); if (commandLineArray.Length <= 0) { throw new ArgumentException("Empty command name or arguments", nameof(commandArguments)); @@ -194,7 +193,7 @@ public string GetDetailedHelp(string commandName, IServiceProvider services, int { if (group.TryGetCommand(commandName, out Command command)) { - if (command.Handler is CommandHandler handler) + if (command.Action is CommandHandler handler) { try { @@ -275,8 +274,7 @@ public void AddCommands(Type type, Func factory) /// private sealed class CommandGroup { - private Parser _parser; - private readonly CommandLineBuilder _rootBuilder; + private Command _rootCommand; private readonly Dictionary _commandHandlers = new(); /// @@ -285,7 +283,7 @@ private sealed class CommandGroup /// command prompted used in help message public CommandGroup(string commandPrompt = null) { - _rootBuilder = new CommandLineBuilder(new Command(commandPrompt)); + _rootCommand = new Command(commandPrompt); } /// @@ -297,8 +295,15 @@ public CommandGroup(string commandPrompt = null) /// parsing error internal bool Execute(IReadOnlyList commandLine, IServiceProvider services) { + IConsoleService consoleService = services.GetService(); + CommandLineConfiguration configuration = new(_rootCommand) + { + Output = new ConsoleServiceWrapper(consoleService.Write), + Error = new ConsoleServiceWrapper(consoleService.WriteError) + }; + // Parse the command line and invoke the command - ParseResult parseResult = Parser.Parse(commandLine); + ParseResult parseResult = configuration.Parse(commandLine); if (parseResult.Errors.Count > 0) { @@ -314,10 +319,9 @@ internal bool Execute(IReadOnlyList commandLine, IServiceProvider servic { if (parseResult.CommandResult.Command is Command command) { - if (command.Handler is CommandHandler handler) + if (command.Action is CommandHandler handler) { - InvocationContext context = new(parseResult, new LocalConsole(services.GetService())); - handler.Invoke(context, services); + handler.Invoke(parseResult, services); return true; } } @@ -325,20 +329,17 @@ internal bool Execute(IReadOnlyList commandLine, IServiceProvider servic return false; } - /// - /// Build/return parser - /// - internal Parser Parser => _parser ??= _rootBuilder.Build(); - /// /// Returns all the command handler instances /// internal IEnumerable CommandHandlers => _commandHandlers.Values; + internal Command Parser => _rootCommand; + /// /// Returns true if command or command alias is found /// - internal bool Contains(string commandName) => _rootBuilder.Command.Children.Contains(commandName); + internal bool Contains(string commandName) => TryGetCommand(commandName, out _); /// /// Returns the command handler for the command or command alias @@ -351,7 +352,7 @@ internal bool TryGetCommandHandler(string commandName, out CommandHandler handle handler = null; if (TryGetCommand(commandName, out Command command)) { - handler = command.Handler as CommandHandler; + handler = command.Action as CommandHandler; } return handler != null; } @@ -364,7 +365,7 @@ internal bool TryGetCommandHandler(string commandName, out CommandHandler handle /// true if found internal bool TryGetCommand(string commandName, out Command command) { - command = _rootBuilder.Command.Children.GetByAlias(commandName) as Command; + command = _rootCommand.Subcommands.FirstOrDefault(cmd => cmd.Name == commandName || cmd.Aliases.Contains(commandName)); return command != null; } @@ -382,7 +383,7 @@ internal void CreateCommand(Type type, CommandAttribute commandAttribute, Func p.CanWrite)) @@ -390,16 +391,16 @@ internal void CreateCommand(Type type, CommandAttribute commandAttribute, Func).MakeGenericType(property.PropertyType) + .GetConstructor([typeof(string)]) + .Invoke([argumentAttribute.Name ?? property.Name.ToLowerInvariant()]); + + argument.Description = argumentAttribute.Help; + argument.Arity = arity; + + command.Arguments.Add(argument); arguments.Add((property, argument)); } else @@ -407,37 +408,34 @@ internal void CreateCommand(Type type, CommandAttribute commandAttribute, Func).MakeGenericType(property.PropertyType) + .GetConstructor([typeof(string), typeof(string[])]) + .Invoke([optionAttribute.Name ?? BuildOptionAlias(property.Name), optionAttribute.Aliases]); - foreach (string alias in optionAttribute.Aliases) - { - option.AddAlias(alias); - } + option.Description = optionAttribute.Help; + + command.Options.Add(option); + options.Add((property, option)); } } } CommandHandler handler = new(commandAttribute, arguments, options, type, factory); _commandHandlers.Add(command.Name, handler); - command.Handler = handler; - _rootBuilder.AddCommand(command); + command.Action = handler; + _rootCommand.Subcommands.Add(command); // Build or re-build parser instance after this command is added - FlushParser(); } - internal string GetDetailedHelp(ICommand command, IServiceProvider services, int windowWidth) + internal string GetDetailedHelp(Command command, IServiceProvider services, int windowWidth) { - CaptureConsole console = new(); + StringWriter console = new(); // Get the command help - HelpBuilder helpBuilder = new(console, maxWidth: windowWidth); - helpBuilder.Write(command); + HelpBuilder helpBuilder = new(maxWidth: windowWidth); + HelpContext helpContext = new(helpBuilder, command, console); + helpBuilder.Write(helpContext); // Get the detailed help if any if (TryGetCommandHandler(command.Name, out CommandHandler handler)) @@ -445,15 +443,13 @@ internal string GetDetailedHelp(ICommand command, IServiceProvider services, int string helpText = handler.GetDetailedHelp(Parser, services); if (helpText is not null) { - console.Out.Write(helpText); + console.Write(helpText); } } return console.ToString(); } - private void FlushParser() => _parser = null; - private static string BuildOptionAlias(string parameterName) { if (string.IsNullOrWhiteSpace(parameterName)) @@ -467,7 +463,7 @@ private static string BuildOptionAlias(string parameterName) /// /// The normal command handler. /// - private sealed class CommandHandler : ICommandHandler + private sealed class CommandHandler : SynchronousCommandLineAction { private readonly CommandAttribute _commandAttribute; private readonly IEnumerable<(PropertyInfo Property, Argument Argument)> _arguments; @@ -535,11 +531,6 @@ public CommandHandler( } } - Task ICommandHandler.InvokeAsync(InvocationContext context) - { - return Task.FromException(new NotImplementedException()); - } - /// /// Returns the command name /// @@ -568,14 +559,16 @@ Task ICommandHandler.InvokeAsync(InvocationContext context) /// /// Returns true is the command is supported by the command filter. Calls the FilterInvokeAttribute marked method. /// - internal bool IsCommandSupported(Parser parser, IServiceProvider services) => _methodInfoFilter == null || (bool)Invoke(_methodInfoFilter, context: null, parser, services); + internal bool IsCommandSupported(Command parser, IServiceProvider services) => _methodInfoFilter == null || (bool)Invoke(_methodInfoFilter, context: null, parser, services); /// /// Execute the command synchronously. /// /// invocation context /// service provider - internal void Invoke(InvocationContext context, IServiceProvider services) => Invoke(_methodInfo, context, context.Parser, services); + internal void Invoke(ParseResult context, IServiceProvider services) => Invoke(_methodInfo, context, (Command)context.RootCommandResult.Command, services); + + public override int Invoke(ParseResult parseResult) => throw new NotImplementedException(); /// /// Return the various ways the command can be invoked. For building the help text. @@ -604,7 +597,7 @@ internal string HelpInvocation /// parser instance /// service provider /// true help called, false no help function - internal string GetDetailedHelp(Parser parser, IServiceProvider services) + internal string GetDetailedHelp(Command parser, IServiceProvider services) { if (_methodInfoHelp == null) { @@ -618,7 +611,7 @@ internal string GetDetailedHelp(Parser parser, IServiceProvider services) return (string)Invoke(_methodInfoHelp, context: null, parser, services); } - private object Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services) + private object Invoke(MethodInfo methodInfo, ParseResult context, Command parser, IServiceProvider services) { object instance = null; if (!methodInfo.IsStatic) @@ -629,7 +622,7 @@ private object Invoke(MethodInfo methodInfo, InvocationContext context, Parser p return Utilities.Invoke(methodInfo, instance, services); } - private void SetProperties(InvocationContext context, Parser parser, object instance) + private void SetProperties(ParseResult contextParseResult, Command parser, object instance) { ParseResult defaultParseResult = null; @@ -637,7 +630,9 @@ private void SetProperties(InvocationContext context, Parser parser, object inst string defaultOptions = _commandAttribute.DefaultOptions; if (defaultOptions != null) { - defaultParseResult = parser.Parse(Name + " " + defaultOptions); + List commandLine = new() { Name }; + commandLine.AddRange(CommandLineParser.SplitCommandLine(defaultOptions)); + defaultParseResult = parser.Parse(commandLine); } // Now initialize the option and service properties from the default and command line options @@ -647,18 +642,18 @@ private void SetProperties(InvocationContext context, Parser parser, object inst if (defaultParseResult != null) { - OptionResult defaultOptionResult = defaultParseResult.FindResultFor(option.Option); + OptionResult defaultOptionResult = defaultParseResult.GetResult(option.Option); if (defaultOptionResult != null) { - value = defaultOptionResult.GetValueOrDefault(); + value = defaultOptionResult.GetValueOrDefault(); } } - if (context != null) + if (contextParseResult != null) { - OptionResult optionResult = context.ParseResult.FindResultFor(option.Option); + OptionResult optionResult = contextParseResult.GetResult(option.Option); if (optionResult != null) { - value = optionResult.GetValueOrDefault(); + value = optionResult.GetValueOrDefault(); } } @@ -682,22 +677,22 @@ private void SetProperties(InvocationContext context, Parser parser, object inst if (defaultParseResult != null) { - ArgumentResult defaultArgumentResult = defaultParseResult.FindResultFor(argument.Argument); + ArgumentResult defaultArgumentResult = defaultParseResult.GetResult(argument.Argument); if (defaultArgumentResult != null) { - value = defaultArgumentResult.GetValueOrDefault(); + value = defaultArgumentResult.GetValueOrDefault(); if (array != null && value is IEnumerable entries) { array.AddRange(entries); } } } - if (context != null) + if (contextParseResult != null) { - ArgumentResult argumentResult = context.ParseResult.FindResultFor(argument.Argument); + ArgumentResult argumentResult = contextParseResult.GetResult(argument.Argument); if (argumentResult != null) { - value = argumentResult.GetValueOrDefault(); + value = argumentResult.GetValueOrDefault(); if (array != null && value is IEnumerable entries) { array.AddRange(entries); @@ -710,71 +705,15 @@ private void SetProperties(InvocationContext context, Parser parser, object inst } } - /// - /// IConsole implementation that captures all the output into a string. - /// - private sealed class CaptureConsole : IConsole - { - private readonly StringBuilder _builder = new(); - - public CaptureConsole() - { - Out = Error = new StandardStreamWriter((text) => _builder.Append(text)); - } - - public override string ToString() => _builder.ToString(); - - #region IConsole - - public IStandardStreamWriter Out { get; } - - bool IStandardOut.IsOutputRedirected { get { return false; } } - - public IStandardStreamWriter Error { get; } - - bool IStandardError.IsErrorRedirected { get { return false; } } - - bool IStandardIn.IsInputRedirected { get { return false; } } - - #endregion - } - - /// - /// This class wraps the IConsoleService and provides the IConsole interface for System.CommandLine. - /// - private sealed class LocalConsole : IConsole + internal sealed class ConsoleServiceWrapper : TextWriter { - private readonly IConsoleService _consoleService; + private Action _write; - public LocalConsole(IConsoleService consoleService) - { - _consoleService = consoleService; - Out = new StandardStreamWriter(_consoleService.Write); - Error = new StandardStreamWriter(_consoleService.WriteError); - } - - #region IConsole - - public IStandardStreamWriter Out { get; } - - bool IStandardOut.IsOutputRedirected { get { return false; } } - - public IStandardStreamWriter Error { get; } - - bool IStandardError.IsErrorRedirected { get { return false; } } - - bool IStandardIn.IsInputRedirected { get { return false; } } - - #endregion - } - - private sealed class StandardStreamWriter : IStandardStreamWriter - { - private readonly Action _write; + public ConsoleServiceWrapper(Action write) => _write = write; - public StandardStreamWriter(Action write) => _write = write; + public override void Write(string value) => _write.Invoke(value); - void IStandardStreamWriter.Write(string value) => _write(value); + public override Encoding Encoding => throw new NotImplementedException(); } } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/StringExtensions.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/StringExtensions.cs new file mode 100644 index 0000000000..6e07f59bbc --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/StringExtensions.cs @@ -0,0 +1,64 @@ +// 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; + +namespace System.CommandLine +{ + // class copied from https://raw.githubusercontent.com/dotnet/command-line-api/060374e56c1b2e741b6525ca8417006efb54fbd7/src/System.CommandLine.DragonFruit/StringExtensions.cs + internal static class StringExtensions + { + public static string ToKebabCase(this string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + StringBuilder sb = new(); + int i = 0; + bool addDash = false; + + // handles beginning of string, breaks on first letter or digit. addDash might be better named "canAddDash" + for (; i < value.Length; i++) + { + char ch = value[i]; + if (char.IsLetterOrDigit(ch)) + { + addDash = !char.IsUpper(ch); + sb.Append(char.ToLowerInvariant(ch)); + i++; + break; + } + } + + // reusing i, start at the same place + for (; i < value.Length; i++) + { + char ch = value[i]; + if (char.IsUpper(ch)) + { + if (addDash) + { + addDash = false; + sb.Append('-'); + } + + sb.Append(char.ToLowerInvariant(ch)); + } + else if (char.IsLetterOrDigit(ch)) + { + addDash = true; + sb.Append(ch); + } + else //this coverts all non letter/digits to dash - specifically periods and underscores. Is this needed? + { + addDash = false; + sb.Append('-'); + } + } + + return sb.ToString(); + } + } +} diff --git a/src/SOS/SOS.UnitTests/Scripts/DumpGen.script b/src/SOS/SOS.UnitTests/Scripts/DumpGen.script index 525bfb897d..412d069d0d 100644 --- a/src/SOS/SOS.UnitTests/Scripts/DumpGen.script +++ b/src/SOS/SOS.UnitTests/Scripts/DumpGen.script @@ -17,7 +17,7 @@ VERIFY: invalid is not a supported generation !IFDEF:LLDB EXTCOMMAND_FAIL: dumpgen gen0 -mt -VERIFY: Required argument missing for option: -mt +VERIFY: Required argument missing for option: '-mt'. EXTCOMMAND_FAIL: dumpgen gen1 -mt zzzzz VERIFY: Hexadecimal address expected for -mt option diff --git a/src/Tools/Common/CommandExtensions.cs b/src/Tools/Common/CommandExtensions.cs deleted file mode 100644 index 58f91ce579..0000000000 --- a/src/Tools/Common/CommandExtensions.cs +++ /dev/null @@ -1,39 +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.Builder; -using System.CommandLine.Invocation; - -namespace Microsoft.Internal.Common -{ - public static class CommandExtensions - { - /// - /// Allows the command handler to be included in the collection initializer. - /// - public static void Add(this Command command, ICommandHandler handler) - { - command.Handler = handler; - } - - /// - /// Setups the diagnostic tools defaults. Like .UseDefault except RegisterWithDotnetSuggest() which - /// causes problems on Linux systems with R/O /tmp directory. - /// - public static CommandLineBuilder UseToolsDefaults(this CommandLineBuilder builder) - { - return builder - .UseVersionOption() - .UseHelp() - .UseEnvironmentVariableDirective() - .UseParseDirective() - .UseDebugDirective() - .UseSuggestDirective() - .UseTypoCorrections() - .UseParseErrorReporting() - .UseExceptionHandler() - .CancelOnProcessTermination(); - } - } -} diff --git a/src/Tools/Common/Commands/ProcessStatus.cs b/src/Tools/Common/Commands/ProcessStatus.cs index 524dc15cb7..e5fa27cd97 100644 --- a/src/Tools/Common/Commands/ProcessStatus.cs +++ b/src/Tools/Common/Commands/ProcessStatus.cs @@ -4,30 +4,28 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.IO; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading.Tasks; using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Internal.Common.Utils; using Microsoft.Internal.Common; +using Microsoft.Internal.Common.Utils; using Process = System.Diagnostics.Process; namespace Microsoft.Internal.Common.Commands { public class ProcessStatusCommandHandler { - public static Command ProcessStatusCommand(string description) => - new(name: "ps", description) - { - HandlerDescriptor.FromDelegate((ProcessStatusDelegate)ProcessStatus).GetCommandHandler() - }; - - private delegate void ProcessStatusDelegate(IConsole console); + public static Command ProcessStatusCommand(string description) + { + Command statusCommand = new(name: "ps", description); + statusCommand.SetAction((parseResult, ct) => Task.FromResult(ProcessStatus(parseResult.Configuration.Output, parseResult.Configuration.Error))); + return statusCommand; + } private static void MakeFixedWidth(string text, int width, StringBuilder sb, bool leftPad = false, bool truncateFront = false) { @@ -76,7 +74,7 @@ private struct ProcessDetails /// /// Print the current list of available .NET core processes for diagnosis, their statuses and the command line arguments that are passed to them. /// - public static void ProcessStatus(IConsole console) + public static int ProcessStatus(TextWriter stdOut, TextWriter stdError) { int GetColumnWidth(IEnumerable fieldWidths) { @@ -170,11 +168,13 @@ void FormatTableRows(List rows, StringBuilder tableText) } } FormatTableRows(printInfo, sb); - console.Out.WriteLine(sb.ToString()); + stdOut.WriteLine(sb.ToString()); + return 0; } catch (Exception ex) { - console.Out.WriteLine(ex.ToString()); + stdError.WriteLine(ex.ToString()); + return 1; } } diff --git a/src/Tools/Common/Rendering/Interop.Windows.cs b/src/Tools/Common/Rendering/Interop.Windows.cs new file mode 100644 index 0000000000..7121b789e8 --- /dev/null +++ b/src/Tools/Common/Rendering/Interop.Windows.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. + +using System.Runtime.InteropServices; + +namespace System.CommandLine.Rendering +{ + internal static partial class Interop + { + public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + + public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; + + public const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008; + + public const int STD_OUTPUT_HANDLE = -11; + + public const int STD_INPUT_HANDLE = -10; + + [LibraryImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool GetConsoleMode(IntPtr handle, out uint mode); + + [LibraryImport("kernel32.dll")] + public static partial uint GetLastError(); + + [LibraryImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool SetConsoleMode(IntPtr handle, uint mode); + + [LibraryImport("kernel32.dll", SetLastError = true)] + public static partial IntPtr GetStdHandle(int handle); + } +} diff --git a/src/Tools/Common/Rendering/VirtualTerminalMode.cs b/src/Tools/Common/Rendering/VirtualTerminalMode.cs new file mode 100644 index 0000000000..6c6875834f --- /dev/null +++ b/src/Tools/Common/Rendering/VirtualTerminalMode.cs @@ -0,0 +1,105 @@ +// 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.InteropServices; +using static System.CommandLine.Rendering.Interop; + +namespace System.CommandLine.Rendering +{ + /// + /// This file is a copy of https://github.com/dotnet/command-line-api/blob/060374e56c1b2e741b6525ca8417006efb54fbd7/src/System.CommandLine.Rendering/Interop.Windows.cs + /// which is no longer supported. + /// + public sealed class VirtualTerminalMode : IDisposable + { + private readonly IntPtr _stdOutHandle; + private readonly IntPtr _stdInHandle; + private readonly uint _originalOutputMode; + private readonly uint _originalInputMode; + + private VirtualTerminalMode(bool isEnabled) + { + IsEnabled = isEnabled; + GC.SuppressFinalize(this); // ctor used only on Unix, where there is nothing to cleanup + } + + private VirtualTerminalMode( + IntPtr stdOutHandle, + uint originalOutputMode, + IntPtr stdInHandle, + uint originalInputMode) + { + _stdOutHandle = stdOutHandle; + _originalOutputMode = originalOutputMode; + _stdInHandle = stdInHandle; + _originalInputMode = originalInputMode; + } + + public bool IsEnabled { get; } + + public static VirtualTerminalMode TryEnable() + { + if (OperatingSystem.IsWindows()) + { + IntPtr stdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + IntPtr stdInHandle = GetStdHandle(STD_INPUT_HANDLE); + + if (!GetConsoleMode(stdOutHandle, out uint originalOutputMode)) + { + return null; + } + + if (!GetConsoleMode(stdInHandle, out uint originalInputMode)) + { + return null; + } + + uint requestedOutputMode = originalOutputMode | + ENABLE_VIRTUAL_TERMINAL_PROCESSING | + DISABLE_NEWLINE_AUTO_RETURN; + + if (!SetConsoleMode(stdOutHandle, requestedOutputMode)) + { + return null; + } + + return new VirtualTerminalMode(stdOutHandle, + originalOutputMode, + stdInHandle, + originalInputMode); + } + else + { + string terminalName = Environment.GetEnvironmentVariable("TERM"); + + bool isXterm = !string.IsNullOrEmpty(terminalName) + && terminalName.StartsWith("xterm", StringComparison.OrdinalIgnoreCase); + + // TODO: Is this a reasonable default? + return new VirtualTerminalMode(isXterm); + } + } + + private void RestoreConsoleMode() + { + if (IsEnabled) + { + if (_stdOutHandle != IntPtr.Zero) + { + SetConsoleMode(_stdOutHandle, _originalOutputMode); + } + } + } + + public void Dispose() + { + RestoreConsoleMode(); + GC.SuppressFinalize(this); + } + + ~VirtualTerminalMode() + { + RestoreConsoleMode(); + } + } +} diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index b57bfec95d..14e745f717 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.IO; using System.CommandLine.Rendering; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +16,6 @@ using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tools.Counters.Exporters; using Microsoft.Internal.Common.Utils; -using IConsole = System.CommandLine.IConsole; namespace Microsoft.Diagnostics.Tools.Counters { @@ -25,8 +24,9 @@ internal class CounterMonitor : ICountersLogger private const int BufferDelaySecs = 1; private const string EventCountersProviderPrefix = "EventCounters\\"; private int _processId; + private TextWriter _stdOutput; + private TextWriter _stdError; private List _counterList; - private IConsole _console; private ICounterRenderer _renderer; private string _output; private bool _pauseCmdSet; @@ -42,10 +42,12 @@ private class ProviderEventState private readonly Dictionary _providerEventStates = new(); private readonly Queue _bufferedEvents = new(); - public CounterMonitor() + public CounterMonitor(TextWriter stdOutput, TextWriter stdError) { _pauseCmdSet = false; _shouldExit = new TaskCompletionSource(); + _stdOutput = stdOutput; + _stdError = stdError; } private void MeterInstrumentEventObserved(string meterName, DateTime timestamp) @@ -166,7 +168,6 @@ public async Task Monitor( CancellationToken ct, List counter_list, string counters, - IConsole console, int processId, int refreshInterval, string name, @@ -202,7 +203,6 @@ public async Task Monitor( } try { - _console = console; // the launch command may misinterpret app arguments as the old space separated // provider list so we need to ignore it in that case _counterList = ConfigureCounters(counters, _processId != 0 ? counter_list : null); @@ -237,14 +237,14 @@ public async Task Monitor( { //Cancellation token should automatically stop the session - console.Out.WriteLine($"Complete"); + _stdOutput.WriteLine($"Complete"); return ReturnCode.Ok; } } } catch (CommandLineErrorException e) { - console.Error.WriteLine(e.Message); + _stdError.WriteLine(e.Message); return ReturnCode.ArgumentError; } } @@ -252,7 +252,6 @@ public async Task Collect( CancellationToken ct, List counter_list, string counters, - IConsole console, int processId, int refreshInterval, CountersExportFormat format, @@ -288,7 +287,6 @@ public async Task Collect( try { - _console = console; // the launch command may misinterpret app arguments as the old space separated // provider list so we need to ignore it in that case _counterList = ConfigureCounters(counters, _processId != 0 ? counter_list : null); @@ -303,7 +301,7 @@ public async Task Collect( _diagnosticsClient = holder.Client; if (_output.Length == 0) { - _console.Error.WriteLine("Output cannot be an empty string"); + _stdError.WriteLine("Output cannot be an empty string"); return ReturnCode.ArgumentError; } if (format == CountersExportFormat.csv) @@ -327,7 +325,7 @@ public async Task Collect( } else { - _console.Error.WriteLine($"The output format {format} is not a valid output format."); + _stdError.WriteLine($"The output format {format} is not a valid output format."); return ReturnCode.ArgumentError; } @@ -349,7 +347,7 @@ public async Task Collect( } catch (CommandLineErrorException e) { - console.Error.WriteLine(e.Message); + _stdError.WriteLine(e.Message); return ReturnCode.ArgumentError; } } @@ -398,7 +396,7 @@ internal List ConfigureCounters(string commaSeparatedProv if (counters.Count == 0) { - _console.Out.WriteLine($"--counters is unspecified. Monitoring System.Runtime counters by default."); + _stdOutput.WriteLine($"--counters is unspecified. Monitoring System.Runtime counters by default."); ParseCounterProvider("System.Runtime", counters); } return counters; diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index b9079cefbb..14ce1d38ac 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -4,9 +4,6 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; using System.Threading; @@ -21,207 +18,201 @@ public enum CountersExportFormat { csv, json }; internal static class Program { - private delegate Task CollectDelegate( - CancellationToken ct, - List counter_list, - string counters, - IConsole console, - int processId, - int refreshInterval, - CountersExportFormat format, - string output, - string processName, - string port, - bool resumeRuntime, - int maxHistograms, - int maxTimeSeries, - TimeSpan duration); - - private delegate Task MonitorDelegate( - CancellationToken ct, - List counter_list, - string counters, - IConsole console, - int processId, - int refreshInterval, - string processName, - string port, - bool resumeRuntime, - int maxHistograms, - int maxTimeSeries, - TimeSpan duration, - bool showDeltas); - - private static Command MonitorCommand() => - new( + private static Command MonitorCommand() + { + Command monitorCommand = new( name: "monitor", description: "Start monitoring a .NET application") { - // Handler - HandlerDescriptor.FromDelegate((MonitorDelegate)new CounterMonitor().Monitor).GetCommandHandler(), - // Arguments and Options - CounterList(), - CounterOption(), - ProcessIdOption(), - RefreshIntervalOption(), - NameOption(), - DiagnosticPortOption(), - ResumeRuntimeOption(), - MaxHistogramOption(), - MaxTimeSeriesOption(), - DurationOption(), - ShowDeltasOption() - }; + CounterList, + CounterOption, + ProcessIdOption, + RefreshIntervalOption, + NameOption, + DiagnosticPortOption, + ResumeRuntimeOption, + MaxHistogramOption, + MaxTimeSeriesOption, + DurationOption, + ShowDeltasOption + }; + + monitorCommand.TreatUnmatchedTokensAsErrors = false; // see the logic in Main + + monitorCommand.SetAction((parseResult, ct) => new CounterMonitor(parseResult.Configuration.Output, parseResult.Configuration.Error).Monitor( + ct, + counter_list: parseResult.GetValue(CounterList), + counters: parseResult.GetValue(CounterOption), + processId: parseResult.GetValue(ProcessIdOption), + refreshInterval: parseResult.GetValue(RefreshIntervalOption), + name: parseResult.GetValue(NameOption), + diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty, + resumeRuntime: parseResult.GetValue(ResumeRuntimeOption), + maxHistograms: parseResult.GetValue(MaxHistogramOption), + maxTimeSeries: parseResult.GetValue(MaxTimeSeriesOption), + duration: parseResult.GetValue(DurationOption), + showDeltas: parseResult.GetValue(ShowDeltasOption) + )); + + return monitorCommand; + } - private static Command CollectCommand() => - new( + private static Command CollectCommand() + { + Command collectCommand = new( name: "collect", description: "Monitor counters in a .NET application and export the result into a file") { - // Handler - HandlerDescriptor.FromDelegate((CollectDelegate)new CounterMonitor().Collect).GetCommandHandler(), - // Arguments and Options - CounterList(), - CounterOption(), - ProcessIdOption(), - RefreshIntervalOption(), - ExportFormatOption(), - ExportFileNameOption(), - NameOption(), - DiagnosticPortOption(), - ResumeRuntimeOption(), - MaxHistogramOption(), - MaxTimeSeriesOption(), - DurationOption() - }; + CounterList, + CounterOption, + ProcessIdOption, + RefreshIntervalOption, + ExportFormatOption, + ExportFileNameOption, + NameOption, + DiagnosticPortOption, + ResumeRuntimeOption, + MaxHistogramOption, + MaxTimeSeriesOption, + DurationOption + }; + + collectCommand.TreatUnmatchedTokensAsErrors = false; // see the logic in Main + + collectCommand.SetAction((parseResult, ct) => new CounterMonitor(parseResult.Configuration.Output, parseResult.Configuration.Error).Collect( + ct, + counter_list: parseResult.GetValue(CounterList), + counters: parseResult.GetValue(CounterOption), + processId: parseResult.GetValue(ProcessIdOption), + refreshInterval: parseResult.GetValue(RefreshIntervalOption), + format: parseResult.GetValue(ExportFormatOption), + output: parseResult.GetValue(ExportFileNameOption), + name: parseResult.GetValue(NameOption), + diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty, + resumeRuntime: parseResult.GetValue(ResumeRuntimeOption), + maxHistograms: parseResult.GetValue(MaxHistogramOption), + maxTimeSeries: parseResult.GetValue(MaxTimeSeriesOption), + duration: parseResult.GetValue(DurationOption))); + + return collectCommand; + } - private static Option NameOption() => - new( - aliases: new[] { "-n", "--name" }, - description: "The name of the process that will be monitored.") + private static readonly Option NameOption = + new("--name", "-n") { - Argument = new Argument(name: "name") + Description = "The name of the process that will be monitored.", }; - private static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id that will be monitored.") + private static Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid") + Description = "The process id that will be monitored.", }; - private static Option RefreshIntervalOption() => - new( - alias: "--refresh-interval", - description: "The number of seconds to delay between updating the displayed counters.") + private static Option RefreshIntervalOption = + new("--refresh-interval") { - Argument = new Argument(name: "refresh-interval", getDefaultValue: () => 1) + Description = "The number of seconds to delay between updating the displayed counters.", + DefaultValueFactory = _ => 1 }; - private static Option ExportFormatOption() => - new( - alias: "--format", - description: "The format of exported counter data.") + private static readonly Option ExportFormatOption = + new("--format") { - Argument = new Argument(name: "format", getDefaultValue: () => CountersExportFormat.csv) + Description = "The format of exported counter data.", + DefaultValueFactory = _ => CountersExportFormat.csv }; - private static Option ExportFileNameOption() => - new( - aliases: new[] { "-o", "--output" }, - description: "The output file name.") + private static readonly Option ExportFileNameOption = + new("--output", "-o") { - Argument = new Argument(name: "output", getDefaultValue: () => "counter") + Description = "The output file name.", + DefaultValueFactory = _ => "counter" }; - private static Option CounterOption() => - new( - alias: "--counters", - description: "A comma-separated list of counter providers. Counter providers can be specified as or [comma_separated_counter_names]. If the provider_name" + + private static readonly Option CounterOption = + new("--counters") + { + Description = "A comma-separated list of counter providers. Counter providers can be specified as or [comma_separated_counter_names]. If the provider_name" + " is used without qualifying counter_names then all counters will be shown. For example \"System.Runtime[dotnet.assembly.count,dotnet.gc.pause.time],Microsoft.AspNetCore.Hosting\"" + " includes the dotnet.assembly.count and dotnet.gc.pause.time counters from the System.Runtime provider and all the counters from the Microsoft.AspNetCore.Hosting provider. Provider" + " names can either refer to the name of a Meter for the System.Diagnostics.Metrics API or the name of an EventSource for the EventCounters API. If the monitored application has both" + " a Meter and an EventSource with the same name, the Meter is automatically preferred. Use the prefix \'EventCounters\\\' in front of a provider name to only show the EventCounters." + - " To discover well-known provider and counter names, please visit https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics.") - { - Argument = new Argument(name: "counters") + " To discover well-known provider and counter names, please visit https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics." }; - private static Argument CounterList() => - new Argument>(name: "counter_list", getDefaultValue: () => new List()) + private static readonly Argument> CounterList = + new(name: "counter_list") { Description = @"A space separated list of counter providers. Counters can be specified or [comma_separated_counter_names]. If the provider_name is used without a qualifying counter_names then all counters will be shown. To discover provider and counter names, use the list command.", - IsHidden = true + Hidden = true, + DefaultValueFactory = _ => new List() }; - private static Command ListCommand() => - new( + private static Command ListCommand() + { + Command listCommand = new( name: "list", description: "Display a list of counter names and descriptions, grouped by provider.") { - CommandHandler.Create(List), RuntimeVersionOption() }; - private static Option RuntimeVersionOption() => - new( - aliases: new[] { "-r", "--runtime-version" }, - description: "Version of runtime. Supported runtime version: 3.0, 3.1, 5.0, 6.0, 7.0, 8.0") + listCommand.SetAction((parseResult, ct) => Task.FromResult(List())); + + return listCommand; + } + + private static Option RuntimeVersionOption() => + new("--runtime-version", "-r") { - Argument = new Argument(name: "runtimeVersion", getDefaultValue: () => "6.0") + Description = "Version of runtime. Supported runtime version: 3.0, 3.1, 5.0, 6.0, 7.0, 8.0", + DefaultValueFactory = _ => "6.0" }; - private static Option DiagnosticPortOption() => - new( - aliases: new[] { "--dport", "--diagnostic-port" }, - description: "The path to diagnostic port to be used.") + private static readonly Option DiagnosticPortOption = + new("--diagnostic-port", "--dport") { - Argument = new Argument(name: "diagnosticPort", getDefaultValue: () => "") + Description = "The path to diagnostic port to be used.", }; - private static Option ResumeRuntimeOption() => - new( - alias: "--resume-runtime", - description: @"Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false") + private static readonly Option ResumeRuntimeOption = + new("--resume-runtime") { - Argument = new Argument(name: "resumeRuntime", getDefaultValue: () => true) + Description = "Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false", + DefaultValueFactory = _ => true }; - private static Option MaxHistogramOption() => - new( - alias: "--maxHistograms", - description: "The maximum number of histograms that can be tracked. Each unique combination of provider name, histogram name, and dimension values" + - " counts as one histogram. Tracking more histograms uses more memory in the target process so this bound guards against unintentional high memory use.") + private static readonly Option MaxHistogramOption = + new("--maxHistograms") { - Argument = new Argument(name: "maxHistograms", getDefaultValue: () => 10) + Description = "The maximum number of histograms that can be tracked. Each unique combination of provider name, histogram name, and dimension values" + + " counts as one histogram. Tracking more histograms uses more memory in the target process so this bound guards against unintentional high memory use.", + DefaultValueFactory = _ => 10 }; - private static Option MaxTimeSeriesOption() => - new( - alias: "--maxTimeSeries", - description: "The maximum number of time series that can be tracked. Each unique combination of provider name, metric name, and dimension values" + - " counts as one time series. Tracking more time series uses more memory in the target process so this bound guards against unintentional high memory use.") + private static readonly Option MaxTimeSeriesOption = + new("--maxTimeSeries") { - Argument = new Argument(name: "maxTimeSeries", getDefaultValue: () => 1000) + Description = "The maximum number of time series that can be tracked. Each unique combination of provider name, metric name, and dimension values" + + " counts as one time series. Tracking more time series uses more memory in the target process so this bound guards against unintentional high memory use.", + DefaultValueFactory = _ => 1000 }; - private static Option DurationOption() => - new( - alias: "--duration", - description: @"When specified, will run for the given timespan and then automatically stop. Provided in the form of dd:hh:mm:ss.") + private static readonly Option DurationOption = + new("--duration") { - Argument = new Argument(name: "duration-timespan", getDefaultValue: () => default) + Description = "When specified, will run for the given timespan and then automatically stop. Provided in the form of dd:hh:mm:ss." }; - private static Option ShowDeltasOption() => - new( - alias: "--showDeltas", - description: @"Shows an extra column in the metrics table that displays the delta between the previous metric value and the most recent value." + - " This is useful to monitor the rate of change for a metric.") - { }; + private static readonly Option ShowDeltasOption = + new("--showDeltas") + { + Description = @"Shows an extra column in the metrics table that displays the delta between the previous metric value and the most recent value." + + " This is useful to monitor the rate of change for a metric." + }; - public static int List(IConsole console, string runtimeVersion) + public static int List() { Console.WriteLine("Counter information has been moved to the online .NET documentation."); Console.WriteLine("Please visit https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics."); @@ -230,19 +221,19 @@ public static int List(IConsole console, string runtimeVersion) private static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(MonitorCommand()) - .AddCommand(CollectCommand()) - .AddCommand(ListCommand()) - .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that can be monitored.")) - .UseToolsDefaults() - .Build(); + RootCommand rootCommand = new() + { + MonitorCommand(), + CollectCommand(), + ListCommand(), + ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that can be monitored.") + }; - ParseResult parseResult = parser.Parse(args); + ParseResult parseResult = rootCommand.Parse(args); string parsedCommandName = parseResult.CommandResult.Command.Name; if (parsedCommandName is "monitor" or "collect") { - IReadOnlyCollection unparsedTokens = parseResult.UnparsedTokens; + IReadOnlyCollection unparsedTokens = parseResult.UnmatchedTokens; // If we notice there are unparsed tokens, user might want to attach on startup. if (unparsedTokens.Count > 0) { @@ -250,7 +241,7 @@ private static Task Main(string[] args) } } - return parser.InvokeAsync(args); + return parseResult.InvokeAsync(); } } } diff --git a/src/Tools/dotnet-counters/dotnet-counters.csproj b/src/Tools/dotnet-counters/dotnet-counters.csproj index 4631fdc3ac..bd21525faa 100644 --- a/src/Tools/dotnet-counters/dotnet-counters.csproj +++ b/src/Tools/dotnet-counters/dotnet-counters.csproj @@ -12,8 +12,9 @@ - + + @@ -29,7 +30,6 @@ - diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index ef5d4541ac..253ad35b4a 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -3,9 +3,6 @@ using System; using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.Builder; -using System.CommandLine.Parsing; using System.Threading; using System.Threading.Tasks; using Microsoft.Internal.Common; @@ -15,262 +12,315 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter { internal sealed class Program { - private delegate Task DiagnosticsServerIpcClientTcpServerRouterDelegate(CancellationToken ct, string ipcClient, string tcpServer, int runtimeTimeoutS, string verbose, string forwardPort); - - private delegate Task DiagnosticsServerIpcServerTcpServerRouterDelegate(CancellationToken ct, string ipcServer, string tcpServer, int runtimeTimeoutS, string verbose, string forwardPort); - - private delegate Task DiagnosticsServerIpcServerTcpClientRouterDelegate(CancellationToken ct, string ipcServer, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); - - private delegate Task DiagnosticsServerIpcClientTcpClientRouterDelegate(CancellationToken ct, string ipcClient, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); - - private delegate Task DiagnosticsServerIpcServerWebSocketServerRouterDelegate(CancellationToken ct, string ipcServer, string webSocket, int runtimeTimeoutS, string verbose); - - private delegate Task DiagnosticsServerIpcClientWebSocketServerRouterDelegate(CancellationToken ct, string ipcClient, string webSocket, int runtimeTimeoutS, string verbose); - - private delegate Task DiagnosticsServerIpcServerIOSSimulatorRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose, bool info); - - private delegate Task DiagnosticsServerIpcServerIOSRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose, bool info); - - private delegate Task DiagnosticsServerIpcServerAndroidEmulatorRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose, bool info); - - private delegate Task DiagnosticsServerIpcServerAndroidRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose, bool info); - - private static Command IpcClientTcpServerRouterCommand() => - new( + private static Command IpcClientTcpServerRouterCommand() + { + Command command = new( name: "client-server", description: "Start a .NET application Diagnostics Server routing local IPC server <--> remote TCP client. " + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + "and a TCP/IP server (accepting runtime TCP client).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientTcpServerRouter).GetCommandHandler(), - // Options - IpcClientAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + IpcClientAddressOption, TcpServerAddressOption, RuntimeTimeoutOption, VerboseOption, ForwardPortOption }; - private static Command IpcServerTcpServerRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcClientTcpServerRouter( + ct, + ipcClient: parseResult.GetValue(IpcClientAddressOption) ?? "", + tcpServer: parseResult.GetValue(TcpServerAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + forwardPort: parseResult.GetValue(ForwardPortOption) ?? "" + )); + + return command; + } + + private static Command IpcServerTcpServerRouterCommand() + { + Command command = new( name: "server-server", description: "Start a .NET application Diagnostics Server routing local IPC client <--> remote TCP client. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP server (accepting runtime TCP client).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter).GetCommandHandler(), - // Options - IpcServerAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + IpcServerAddressOption, TcpServerAddressOption, RuntimeTimeoutOption, VerboseOption, ForwardPortOption }; - private static Command IpcServerTcpClientRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter( + ct, + ipcServer: parseResult.GetValue(IpcServerAddressOption) ?? "", + tcpServer: parseResult.GetValue(TcpServerAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + forwardPort: parseResult.GetValue(ForwardPortOption) ?? "" + )); + + return command; + } + + private static Command IpcServerTcpClientRouterCommand() + { + Command command = new( name: "server-client", description: "Start a .NET application Diagnostics Server routing local IPC client <--> remote TCP server. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP client (connecting runtime TCP server).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpClientRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpClientRouter).GetCommandHandler(), - // Options - IpcServerAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + IpcServerAddressOption, TcpClientAddressOption, RuntimeTimeoutOption, VerboseOption, ForwardPortOption }; - private static Command IpcServerWebSocketServerRouterCommand() => - new( - name: "server-websocket", - description: "Starts a .NET application Diagnostic Server routing local IPC client <--> remote WebSocket client. " + - "Router is configured using an IPC server (connecting to by diagnostic tools) " + - "and a WebSocket server (accepting runtime WebSocket client).") + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerTcpClientRouter( + ct, + ipcServer: parseResult.GetValue(IpcServerAddressOption) ?? "", + tcpClient: parseResult.GetValue(TcpClientAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + forwardPort: parseResult.GetValue(ForwardPortOption) ?? "" + )); + + return command; + } + + private static Command IpcServerWebSocketServerRouterCommand() { - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerWebSocketServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerWebSocketServerRouter).GetCommandHandler(), - // Options - IpcServerAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() - }; - - private static Command IpcClientWebSocketServerRouterCommand() => - new( - name: "client-websocket", - description: "Starts a .NET application Diagnostic Server routing local IPC server <--> remote WebSocket client. " + - "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + - "and a WebSocket server (accepting runtime WebSocket client).") + Command command = new( + name: "server-websocket", + description: "Starts a .NET application Diagnostic Server routing local IPC client <--> remote WebSocket client. " + + "Router is configured using an IPC server (connecting to by diagnostic tools) " + + "and a WebSocket server (accepting runtime WebSocket client).") + { + IpcServerAddressOption, WebSocketURLAddressOption, RuntimeTimeoutOption, VerboseOption + }; + + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerWebSocketServerRouter( + ct, + ipcServer: parseResult.GetValue(IpcServerAddressOption) ?? "", + webSocket: parseResult.GetValue(WebSocketURLAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption) + )); + + return command; + } + + private static Command IpcClientWebSocketServerRouterCommand() + { + Command command = new( + name: "client-websocket", + description: "Starts a .NET application Diagnostic Server routing local IPC server <--> remote WebSocket client. " + + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + + "and a WebSocket server (accepting runtime WebSocket client).") + { + IpcClientAddressOption, WebSocketURLAddressOption, RuntimeTimeoutOption, VerboseOption + }; + + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcClientWebSocketServerRouter( + ct, + ipcClient: parseResult.GetValue(IpcClientAddressOption) ?? "", + webSocket: parseResult.GetValue(WebSocketURLAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption) + )); + + return command; + } + + private static Command IpcClientTcpClientRouterCommand() { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientWebSocketServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientWebSocketServerRouter).GetCommandHandler(), - // Options - IpcClientAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() - }; - - private static Command IpcClientTcpClientRouterCommand() => - new( + Command command = new( name: "client-client", description: "Start a .NET application Diagnostics Server routing local IPC server <--> remote TCP server. " + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + "and a TCP/IP client (connecting runtime TCP server).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpClientRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientTcpClientRouter).GetCommandHandler(), - // Options - IpcClientAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + IpcClientAddressOption, TcpClientAddressOption, RuntimeTimeoutOption, VerboseOption, ForwardPortOption }; - private static Command IOSSimulatorRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcClientTcpClientRouter( + ct, + ipcClient: parseResult.GetValue(IpcClientAddressOption) ?? "", + tcpClient: parseResult.GetValue(TcpClientAddressOption) ?? "", + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + forwardPort: parseResult.GetValue(ForwardPortOption) ?? "" + )); + + return command; + } + + private static Command IOSSimulatorRouterCommand() + { + Command command = new( name: "ios-sim", description: "Start a .NET application Diagnostics Server routing local IPC server <--> iOS Simulator. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP server (accepting runtime TCP client).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerIOSSimulatorRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerIOSSimulatorRouter).GetCommandHandler(), - // Options - RuntimeTimeoutOption(), VerboseOption(), InfoOption() + RuntimeTimeoutOption, VerboseOption, InfoOption }; - private static Command IOSRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerIOSSimulatorRouter( + ct, + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + info: parseResult.GetValue(InfoOption) + )); + + return command; + } + + private static Command IOSRouterCommand() + { + Command command = new( name: "ios", description: "Start a .NET application Diagnostics Server routing local IPC server <--> iOS Device over usbmux. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP client (connecting runtime TCP server over usbmux).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerIOSRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerIOSRouter).GetCommandHandler(), - // Options - RuntimeTimeoutOption(), VerboseOption(), InfoOption() + RuntimeTimeoutOption, VerboseOption, InfoOption }; - private static Command AndroidEmulatorRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerIOSRouter( + ct, + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + info: parseResult.GetValue(InfoOption) + )); + + return command; + } + + private static Command AndroidEmulatorRouterCommand() + { + Command command = new( name: "android-emu", description: "Start a .NET application Diagnostics Server routing local IPC server <--> Android Emulator. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP server (accepting runtime TCP client).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerAndroidEmulatorRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerAndroidEmulatorRouter).GetCommandHandler(), - // Options - RuntimeTimeoutOption(), VerboseOption(), InfoOption() + RuntimeTimeoutOption, VerboseOption, InfoOption }; - private static Command AndroidRouterCommand() => - new( + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerAndroidEmulatorRouter( + ct, + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + info: parseResult.GetValue(InfoOption))); + + return command; + } + + private static Command AndroidRouterCommand() + { + Command command = new( name: "android", description: "Start a .NET application Diagnostics Server routing local IPC server <--> Android Device. " + "Router is configured using an IPC server (connecting to by diagnostic tools) " + "and a TCP/IP server (accepting runtime TCP client).") { - // Handler - HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerAndroidRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerAndroidRouter).GetCommandHandler(), - // Options - RuntimeTimeoutOption(), VerboseOption(), InfoOption() + RuntimeTimeoutOption, VerboseOption, InfoOption }; - private static Option IpcClientAddressOption() => - new( - aliases: new[] { "--ipc-client", "-ipcc" }, - description: "The diagnostic tool diagnostics server ipc address (--diagnostic-port argument). " + - "Router connects diagnostic tool ipc server when establishing a " + - "new route between runtime and diagnostic tool.") + command.SetAction((parseResult, ct) => new DiagnosticsServerRouterCommands().RunIpcServerAndroidRouter( + ct, + runtimeTimeout: parseResult.GetValue(RuntimeTimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + info: parseResult.GetValue(InfoOption))); + + return command; + } + + private static readonly Option IpcClientAddressOption = + new("--ipc-client", "-ipcc") { - Argument = new Argument(name: "ipcClient", getDefaultValue: () => "") + Description = "The diagnostic tool diagnostics server ipc address (--diagnostic-port argument). " + + "Router connects diagnostic tool ipc server when establishing a " + + "new route between runtime and diagnostic tool." }; - private static Option IpcServerAddressOption() => - new( - aliases: new[] { "--ipc-server", "-ipcs" }, - description: "The diagnostics server ipc address to route. Router accepts ipc connections from diagnostic tools " + - "establishing a new route between runtime and diagnostic tool. If not specified " + - "router will use default ipc diagnostics server path.") + private static readonly Option IpcServerAddressOption = + new("--ipc-server", "-ipcs") { - Argument = new Argument(name: "ipcServer", getDefaultValue: () => "") + Description = "The diagnostics server ipc address to route. Router accepts ipc connections from diagnostic tools " + + "establishing a new route between runtime and diagnostic tool. If not specified " + + "router will use default ipc diagnostics server path." }; - private static Option TcpClientAddressOption() => - new( - aliases: new[] { "--tcp-client", "-tcpc" }, - description: "The runtime TCP/IP address using format [host]:[port]. " + - "Router can can connect 127.0.0.1, [::1], ipv4 address, ipv6 address, hostname addresses." + - "Launch runtime using DOTNET_DiagnosticPorts environment variable to setup listener") + private static readonly Option TcpClientAddressOption = + new("--tcp-client", "-tcpc") { - Argument = new Argument(name: "tcpClient", getDefaultValue: () => "") + Description = "The runtime TCP/IP address using format [host]:[port]. " + + "Router can can connect 127.0.0.1, [::1], ipv4 address, ipv6 address, hostname addresses." + + "Launch runtime using DOTNET_DiagnosticPorts environment variable to setup listener." }; - private static Option TcpServerAddressOption() => - new( - aliases: new[] { "--tcp-server", "-tcps" }, - description: "The router TCP/IP address using format [host]:[port]. " + + private static Option TcpServerAddressOption = + new("--tcp-server", "-tcps") + { + Description = "The router TCP/IP address using format [host]:[port]. " + "Router can bind one (127.0.0.1, [::1], 0.0.0.0, [::], ipv4 address, ipv6 address, hostname) " + "or all (*) interfaces. Launch runtime using DOTNET_DiagnosticPorts environment variable " + - "connecting router TCP server during startup.") - { - Argument = new Argument(name: "tcpServer", getDefaultValue: () => "") + "connecting router TCP server during startup." }; - private static Option WebSocketURLAddressOption() => - new( - aliases: new[] { "--web-socket", "-ws" }, - description: "The router WebSocket address using format ws://[host]:[port]/[path] or wss://[host]:[port]/[path]. " + - "Launch app with WasmExtraConfig property specifying diagnostic_options with a server connect_url") + private static readonly Option WebSocketURLAddressOption = + new("--web-socket", "-ws") { - Argument = new Argument(name: "webSocketURI", getDefaultValue: () => "") + Description = "The router WebSocket address using format ws://[host]:[port]/[path] or wss://[host]:[port]/[path]. " + + "Launch app with WasmExtraConfig property specifying diagnostic_options with a server connect_url" }; - private static Option RuntimeTimeoutOption() => - new( - aliases: new[] { "--runtime-timeout", "-rt" }, - description: "Automatically shutdown router if no runtime connects to it before specified timeout (seconds)." + - "If not specified, router won't trigger an automatic shutdown.") + private static readonly Option RuntimeTimeoutOption = + new("--runtime-timeout", "-rt") { - Argument = new Argument(name: "runtimeTimeout", getDefaultValue: () => Timeout.Infinite) + Description = "Automatically shutdown router if no runtime connects to it before specified timeout (seconds)." + + "If not specified, router won't trigger an automatic shutdown.", + DefaultValueFactory = _ => Timeout.Infinite, }; - private static Option VerboseOption() => - new( - aliases: new[] { "--verbose", "-v" }, - description: "Enable verbose logging (none|critical|error|warning|info|debug|trace)") + private static readonly Option VerboseOption = + new("--verbose", "-v") { - Argument = new Argument(name: "verbose", getDefaultValue: () => "info") + Description = "Enable verbose logging (none|critical|error|warning|info|debug|trace)", + DefaultValueFactory = _ => "info", }; - private static Option ForwardPortOption() => - new( - aliases: new[] { "--forward-port", "-fp" }, - description: "Enable port forwarding, values Android|iOS for TcpClient and only Android for TcpServer. Make sure to set ANDROID_SDK_ROOT before using this option on Android.") + private static readonly Option ForwardPortOption = + new("--forward-port", "-fp") { - Argument = new Argument(name: "forwardPort", getDefaultValue: () => "") + Description = "Enable port forwarding, values Android|iOS for TcpClient and only Android for TcpServer. Make sure to set ANDROID_SDK_ROOT before using this option on Android." }; - private static Option InfoOption() => - new( - aliases: new[] { "--info", "-i" }, - description: "Print info on how to use current dotnet-dsrouter instance with application and diagnostic tooling.") + private static readonly Option InfoOption = + new("--info", "-i") { - Argument = new Argument(name: "info", getDefaultValue: () => false) + Description = "Print info on how to use current dotnet-dsrouter instance with application and diagnostic tooling." }; - private static int Main(string[] args) + private static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(IpcClientTcpServerRouterCommand()) - .AddCommand(IpcServerTcpServerRouterCommand()) - .AddCommand(IpcServerTcpClientRouterCommand()) - .AddCommand(IpcClientTcpClientRouterCommand()) - .AddCommand(IpcServerWebSocketServerRouterCommand()) - .AddCommand(IpcClientWebSocketServerRouterCommand()) - .AddCommand(IOSSimulatorRouterCommand()) - .AddCommand(IOSRouterCommand()) - .AddCommand(AndroidEmulatorRouterCommand()) - .AddCommand(AndroidRouterCommand()) - .UseToolsDefaults() - .Build(); - - ParseResult parseResult = parser.Parse(args); - - if (parseResult.UnparsedTokens.Count > 0) + RootCommand rootCommand = new() + { + IpcClientTcpServerRouterCommand(), + IpcServerTcpServerRouterCommand(), + IpcServerTcpClientRouterCommand(), + IpcClientTcpClientRouterCommand(), + IpcServerWebSocketServerRouterCommand(), + IpcClientWebSocketServerRouterCommand(), + IOSSimulatorRouterCommand(), + IOSRouterCommand(), + AndroidEmulatorRouterCommand(), + AndroidRouterCommand() + }; + + ParseResult parseResult = rootCommand.Parse(args); + + if (parseResult.UnmatchedTokens.Count > 0) { ProcessLauncher.Launcher.PrepareChildProcess(args); } - string verbose = parseResult.ValueForOption("-v"); + string verbose = parseResult.GetValue(VerboseOption); if (!string.Equals(verbose, "none", StringComparison.OrdinalIgnoreCase)) { ConsoleColor currentColor = Console.ForegroundColor; @@ -279,7 +329,7 @@ private static int Main(string[] args) Console.ForegroundColor = currentColor; } - return parser.InvokeAsync(args).Result; + return parseResult.InvokeAsync(); } } } diff --git a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj index 3aade96021..44392c0b4c 100644 --- a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj +++ b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppMinTargetFramework) @@ -11,7 +11,6 @@ - diff --git a/src/Tools/dotnet-dump/Dumper.cs b/src/Tools/dotnet-dump/Dumper.cs index badcdf036e..8f2407c1d9 100644 --- a/src/Tools/dotnet-dump/Dumper.cs +++ b/src/Tools/dotnet-dump/Dumper.cs @@ -3,7 +3,6 @@ using System; using System.CommandLine; -using System.CommandLine.IO; using System.IO; using System.Runtime.InteropServices; using Microsoft.Diagnostics.NETCore.Client; @@ -32,7 +31,7 @@ public Dumper() { } - public int Collect(IConsole console, int processId, string output, bool diag, bool crashreport, DumpTypeOption type, string name) + public int Collect(TextWriter stdOutput, TextWriter stdError, int processId, string output, bool diag, bool crashreport, DumpTypeOption type, string name) { Console.WriteLine(name); if (name != null) @@ -90,7 +89,7 @@ public int Collect(IConsole console, int processId, string output, bool diag, bo dumpTypeMessage = "triage dump"; break; } - console.Out.WriteLine($"Writing {dumpTypeMessage} to {output}"); + stdOutput.WriteLine($"Writing {dumpTypeMessage} to {output}"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -148,11 +147,11 @@ InvalidOperationException or NotSupportedException or DiagnosticsClientException) { - console.Error.WriteLine($"{ex.Message}"); + stdError.WriteLine($"{ex.Message}"); return -1; } - console.Out.WriteLine($"Complete"); + stdOutput.WriteLine($"Complete"); return 0; } } diff --git a/src/Tools/dotnet-dump/Program.cs b/src/Tools/dotnet-dump/Program.cs index 76a6ab7779..f097f28d4e 100644 --- a/src/Tools/dotnet-dump/Program.cs +++ b/src/Tools/dotnet-dump/Program.cs @@ -3,9 +3,6 @@ using System; using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; using System.IO; using System.Threading.Tasks; using Microsoft.Internal.Common; @@ -17,99 +14,102 @@ internal static class Program { public static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(CollectCommand()) - .AddCommand(AnalyzeCommand()) - .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that dumps can be collected from.")) - .UseToolsDefaults() - .Build(); - - return parser.InvokeAsync(args); + RootCommand rootCommand = new() + { + CollectCommand(), + AnalyzeCommand(), + ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that dumps can be collected from.") + }; + + return rootCommand.Parse(args).InvokeAsync(); } - private static Command CollectCommand() => - new(name: "collect", description: "Capture dumps from a process") + private static Command CollectCommand() + { + Command command = new(name: "collect", description: "Capture dumps from a process") { - // Handler - CommandHandler.Create(new Dumper().Collect), - // Options - ProcessIdOption(), OutputOption(), DiagnosticLoggingOption(), CrashReportOption(), TypeOption(), ProcessNameOption() + ProcessIdOption, OutputOption, DiagnosticLoggingOption, CrashReportOption, TypeOption, ProcessNameOption }; - private static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id to collect a memory dump.") + command.SetAction((parseResult, ct) => Task.FromResult(new Dumper().Collect( + stdOutput: parseResult.Configuration.Output, + stdError: parseResult.Configuration.Error, + processId: parseResult.GetValue(ProcessIdOption), + output: parseResult.GetValue(OutputOption), + diag: parseResult.GetValue(DiagnosticLoggingOption), + crashreport: parseResult.GetValue(CrashReportOption), + type: parseResult.GetValue(TypeOption), + name: parseResult.GetValue(ProcessNameOption)))); + + return command; + } + + private static readonly Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid") + Description = "The process id to collect a memory dump." }; - private static Option ProcessNameOption() => - new( - aliases: new[] { "-n", "--name" }, - description: "The name of the process to collect a memory dump.") + private static readonly Option ProcessNameOption = + new("--name", "-n") { - Argument = new Argument(name: "name") + Description = "The name of the process to collect a memory dump." }; - private static Option OutputOption() => - new( - aliases: new[] { "-o", "--output" }, - description: @"The path where collected dumps should be written. Defaults to '.\dump_YYYYMMDD_HHMMSS.dmp' on Windows and './core_YYYYMMDD_HHMMSS' -on Linux where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump.") + private static readonly Option OutputOption = + new("--output", "-o") { - Argument = new Argument(name: "output_dump_path") + Description = @"The path where collected dumps should be written. Defaults to '.\dump_YYYYMMDD_HHMMSS.dmp' on Windows and './core_YYYYMMDD_HHMMSS' +on Linux where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump." }; - private static Option DiagnosticLoggingOption() => - new( - alias: "--diag", - description: "Enable dump collection diagnostic logging.") + private static readonly Option DiagnosticLoggingOption = + new("--diag") { - Argument = new Argument(name: "diag") + Description = "Enable dump collection diagnostic logging." }; - private static Option CrashReportOption() => - new( - alias: "--crashreport", - description: "Enable crash report generation.") + private static readonly Option CrashReportOption = + new("--crashreport") { - Argument = new Argument(name: "crashreport") + Description = "Enable crash report generation." }; - private static Option TypeOption() => - new( - alias: "--type", - description: @"The dump type determines the kinds of information that are collected from the process. There are several types: Full - The largest dump containing all memory including the module images. Heap - A large and relatively comprehensive dump containing module lists, thread lists, all stacks, exception information, handle information, and all memory except for mapped images. Mini - A small dump containing module lists, thread lists, exception information and all stacks. Triage - A small dump containing module lists, thread lists, exception information, all stacks and PII removed.") + private static readonly Option TypeOption = + new("--type") { - Argument = new Argument(name: "dump_type", getDefaultValue: () => Dumper.DumpTypeOption.Full) + Description = @"The dump type determines the kinds of information that are collected from the process. There are several types: Full - The largest dump containing all memory including the module images. Heap - A large and relatively comprehensive dump containing module lists, thread lists, all stacks, exception information, handle information, and all memory except for mapped images. Mini - A small dump containing module lists, thread lists, exception information and all stacks. Triage - A small dump containing module lists, thread lists, exception information, all stacks and PII removed.", + DefaultValueFactory = _ => Dumper.DumpTypeOption.Full }; - private static Command AnalyzeCommand() => - new( + private static Command AnalyzeCommand() + { + Command command = new( name: "analyze", description: "Starts an interactive shell with debugging commands to explore a dump") { - // Handler - CommandHandler.Create(new Analyzer().Analyze), - // Arguments and Options - DumpPath(), - RunCommand() + DumpPath, + RunCommand }; - private static Argument DumpPath() => - new Argument( - name: "dump_path") + command.SetAction((parseResult, ct) => new Analyzer().Analyze( + parseResult.GetValue(DumpPath), + parseResult.GetValue(RunCommand) ?? Array.Empty())); + + return command; + } + + private static readonly Argument DumpPath = + new Argument(name: "dump_path") { Description = "Name of the dump file to analyze." - }.ExistingOnly(); + }.AcceptExistingOnly(); - private static Option RunCommand() => - new( - aliases: new[] { "-c", "--command" }, - description: "Runs the command on start. Multiple instances of this parameter can be used in an invocation to chain commands. Commands will get run in the order that they are provided on the command line. If you want dotnet dump to exit after the commands, your last command should be 'exit'.") + private static readonly Option RunCommand = + new("--command", "-c") { - Argument = new Argument(name: "command", getDefaultValue: () => Array.Empty()) { Arity = ArgumentArity.ZeroOrMore } + Description = "Runs the command on start. Multiple instances of this parameter can be used in an invocation to chain commands. Commands will get run in the order that they are provided on the command line. If you want dotnet dump to exit after the commands, your last command should be 'exit'.", + Arity = ArgumentArity.ZeroOrMore }; } } diff --git a/src/Tools/dotnet-dump/dotnet-dump.csproj b/src/Tools/dotnet-dump/dotnet-dump.csproj index b59b77a51d..5c0a133ba4 100644 --- a/src/Tools/dotnet-dump/dotnet-dump.csproj +++ b/src/Tools/dotnet-dump/dotnet-dump.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs index 5c18fa8063..50c9147f64 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs @@ -3,7 +3,6 @@ using System; using System.CommandLine; -using System.CommandLine.Binding; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -16,8 +15,6 @@ namespace Microsoft.Diagnostics.Tools.GCDump { internal static class CollectCommandHandler { - private delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort); - /// /// Collects a gcdump from a currently running process. /// @@ -30,7 +27,7 @@ internal static class CollectCommandHandler /// The process name to collect the gcdump from. /// The diagnostic IPC channel to collect the gcdump from. /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort) + private static async Task Collect(CancellationToken ct, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort) { if (!CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId)) { @@ -131,69 +128,67 @@ internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, return true; } - public static Command CollectCommand() => - new( + public static Command CollectCommand() + { + Command collectCommand = new( name: "collect", description: "Collects a diagnostic trace from a currently running process") { - // Handler - HandlerDescriptor.FromDelegate((CollectDelegate) Collect).GetCommandHandler(), - // Options - ProcessIdOption(), - OutputPathOption(), - VerboseOption(), - TimeoutOption(), - NameOption(), - DiagnosticPortOption() + ProcessIdOption, + OutputPathOption, + VerboseOption, + TimeoutOption, + NameOption, + DiagnosticPortOption }; - private static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id to collect the gcdump from.") + collectCommand.SetAction(static (parseResult, ct) => Collect(ct, + processId: parseResult.GetValue(ProcessIdOption), + output: parseResult.GetValue(OutputPathOption) ?? string.Empty, + timeout: parseResult.GetValue(TimeoutOption), + verbose: parseResult.GetValue(VerboseOption), + name: parseResult.GetValue(NameOption), + diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty)); + + return collectCommand; + } + + private static readonly Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid"), + Description = "The process id to collect the gcdump from." }; - private static Option NameOption() => - new( - aliases: new[] { "-n", "--name" }, - description: "The name of the process to collect the gcdump from.") + private static readonly Option NameOption = + new("--name", "-n") { - Argument = new Argument(name: "name") + Description = "The name of the process to collect the gcdump from." }; - private static Option OutputPathOption() => - new( - aliases: new[] { "-o", "--output" }, - description: $@"The path where collected gcdumps should be written. Defaults to '.\YYYYMMDD_HHMMSS_.gcdump' where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump.") + private static readonly Option OutputPathOption = + new("--output", "-o") { - Argument = new Argument(name: "gcdump-file-path", getDefaultValue: () => string.Empty) + Description = @"The path where collected gcdumps should be written. Defaults to '.\YYYYMMDD_HHMMSS_.gcdump' where YYYYMMDD is Year/Month/Day and HHMMSS is Hour/Minute/Second. Otherwise, it is the full path and file name of the dump." }; - private static Option VerboseOption() => - new( - aliases: new[] { "-v", "--verbose" }, - description: "Output the log while collecting the gcdump.") + private static readonly Option VerboseOption = + new("--verbose", "-v") { - Argument = new Argument(name: "verbose") + Description = "Output the log while collecting the gcdump." }; public static int DefaultTimeout = 30; - private static Option TimeoutOption() => - new( - aliases: new[] { "-t", "--timeout" }, - description: $"Give up on collecting the gcdump if it takes longer than this many seconds. The default value is {DefaultTimeout}s.") + private static readonly Option TimeoutOption = + new("--timeout", "-t") { - Argument = new Argument(name: "timeout", getDefaultValue: () => DefaultTimeout) + Description = $"Give up on collecting the gcdump if it takes longer than this many seconds. The default value is {DefaultTimeout}s.", + DefaultValueFactory = _ => DefaultTimeout, }; - private static Option DiagnosticPortOption() => - new( - aliases: new[] { "--dport", "--diagnostic-port" }, - description: "The path to a diagnostic port to collect the dump from.") - { - Argument = new Argument(name: "diagnostic-port", getDefaultValue: () => string.Empty) - }; + private static readonly Option DiagnosticPortOption = + new("--diagnostic-port", "--dport") + { + Description = "The path to a diagnostic port to collect the dump from." + }; } } diff --git a/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs index af04a9aba6..ba40246f25 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/ConvertCommandHandler.cs @@ -4,6 +4,7 @@ using System; using System.CommandLine; using System.IO; +using System.Threading.Tasks; using Graphs; using Microsoft.Internal.Common; @@ -53,40 +54,42 @@ public static int ConvertFile(FileInfo input, string output, bool verbose) return 0; } - public static System.CommandLine.Command ConvertCommand() => - new( + public static Command ConvertCommand() + { + Command convertCommand = new( name: "convert", description: "Converts nettrace file into .gcdump file handled by analysis tools. Can only convert from the nettrace format.") { - // Handler - System.CommandLine.Invocation.CommandHandler.Create(ConvertFile), - // Arguments and Options - InputPathArgument(), - OutputPathOption(), - VerboseOption() + InputPathArgument, + OutputPathOption, + VerboseOption }; - private static Argument InputPathArgument() => + convertCommand.SetAction((parseResult, ct) => Task.FromResult(ConvertFile( + input: parseResult.GetValue(InputPathArgument), + output: parseResult.GetValue(OutputPathOption) ?? string.Empty, + verbose: parseResult.GetValue(VerboseOption)))); + + return convertCommand; + } + + private static readonly Argument InputPathArgument = new Argument("input") { Description = "Input trace file to be converted.", Arity = new ArgumentArity(0, 1) - }.ExistingOnly(); + }.AcceptExistingOnly(); - private static Option OutputPathOption() => - new( - aliases: new[] { "-o", "--output" }, - description: $@"The path where converted gcdump should be written. Defaults to '.gcdump'") + private static readonly Option OutputPathOption = + new("--output", "-o") { - Argument = new Argument(name: "output", getDefaultValue: () => string.Empty) + Description = "The path where converted gcdump should be written. Defaults to '.gcdump'", }; - private static Option VerboseOption() => - new( - aliases: new[] { "-v", "--verbose" }, - description: "Output the log while converting the gcdump.") + private static readonly Option VerboseOption = + new("--verbose", "-v") { - Argument = new Argument(name: "verbose", getDefaultValue: () => false) + Description = "Output the log while converting the gcdump." }; } } diff --git a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs index 538b2ef964..19c26c668a 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs @@ -16,23 +16,29 @@ namespace Microsoft.Diagnostics.Tools.GCDump { internal static class ReportCommandHandler { - private delegate Task ReportDelegate(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType reportType = ReportType.HeapStat, string diagnosticPort = null); - - public static Command ReportCommand() => - new( + public static Command ReportCommand() + { + Command reportCommand = new( name: "report", description: "Generate report into stdout from a previously generated gcdump or from a running process.") { - // Handler - HandlerDescriptor.FromDelegate((ReportDelegate) Report).GetCommandHandler(), - // Options - FileNameArgument(), - ProcessIdOption(), - ReportTypeOption(), - DiagnosticPortOption(), + FileNameArgument, + ProcessIdOption, + ReportTypeOption, + DiagnosticPortOption }; - private static Task Report(CancellationToken ct, IConsole console, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat, string diagnosticPort = null) + reportCommand.SetAction((parseResult, ct) => Report( + ct, + gcdump_filename: parseResult.GetValue(FileNameArgument), + processId: parseResult.GetValue(ProcessIdOption), + type: parseResult.GetValue(ReportTypeOption), + diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty + )); + + return reportCommand; + } + private static Task Report(CancellationToken ct, FileInfo gcdump_filename, int? processId = null, ReportType type = ReportType.HeapStat, string diagnosticPort = null) { // // Validation @@ -145,35 +151,30 @@ private static Task ReportFromFile(FileSystemInfo file) } } - private static Argument FileNameArgument() => + private static readonly Argument FileNameArgument = new Argument("gcdump_filename") { Description = "The file to read gcdump from.", Arity = new ArgumentArity(0, 1) - }.ExistingOnly(); + }.AcceptExistingOnly(); - private static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id to collect the gcdump from.") + private static Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid"), + Description = "The process id to collect the gcdump from.", }; - private static Option ReportTypeOption() => - new( - aliases: new[] { "-t", "--report-type" }, - description: "The type of report to generate. Available options: heapstat (default)") + private static readonly Option ReportTypeOption = + new("--report-type", "-t") { - Argument = new Argument(name: "report-type", () => ReportType.HeapStat) + Description = "The type of report to generate. Available options: heapstat (default)", + DefaultValueFactory = _ => ReportType.HeapStat }; - private static Option DiagnosticPortOption() => - new( - aliases: new[] { "--dport", "--diagnostic-port" }, - description: "The path to a diagnostic port to collect the dump from.") + private static readonly Option DiagnosticPortOption = + new("--diagnostic-port", "--dport") { - Argument = new Argument(name: "diagnostic-port", getDefaultValue: () => string.Empty) + Description = "The path to a diagnostic port to collect the dump from." }; private enum ReportSource diff --git a/src/Tools/dotnet-gcdump/Program.cs b/src/Tools/dotnet-gcdump/Program.cs index dfb1cc9020..47e478beba 100644 --- a/src/Tools/dotnet-gcdump/Program.cs +++ b/src/Tools/dotnet-gcdump/Program.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. -using System.CommandLine.Builder; -using System.CommandLine.Parsing; +using System.CommandLine; using System.Threading.Tasks; using Microsoft.Internal.Common; using Microsoft.Internal.Common.Commands; @@ -13,15 +12,15 @@ internal static class Program { public static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(CollectCommandHandler.CollectCommand()) - .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that gcdumps can be collected from.")) - .AddCommand(ReportCommandHandler.ReportCommand()) - .AddCommand(ConvertCommandHandler.ConvertCommand()) - .UseToolsDefaults() - .Build(); + RootCommand rootCommand = new() + { + CollectCommandHandler.CollectCommand(), + ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that gcdumps can be collected from."), + ReportCommandHandler.ReportCommand(), + ConvertCommandHandler.ConvertCommand() + }; - return parser.InvokeAsync(args); + return rootCommand.Parse(args).InvokeAsync(); } } } diff --git a/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj b/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj index dd1d268d77..ab20ab3540 100644 --- a/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj +++ b/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj @@ -13,7 +13,6 @@ - @@ -22,7 +21,6 @@ - diff --git a/src/Tools/dotnet-sos/Program.cs b/src/Tools/dotnet-sos/Program.cs index 983c81c9ea..c2da2f4e4f 100644 --- a/src/Tools/dotnet-sos/Program.cs +++ b/src/Tools/dotnet-sos/Program.cs @@ -2,62 +2,70 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.CommandLine.Parsing; +using System.IO; using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Microsoft.Internal.Common; using SOS; namespace Microsoft.Diagnostics.Tools.SOS { public class Program { - public static Task Main(string[] args) + public static int Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(InstallCommand()) - .AddCommand(UninstallCommand()) - .UseToolsDefaults() - .Build(); + RootCommand rootCommand = new() + { + InstallCommand(), + UninstallCommand() + }; - return parser.InvokeAsync(args); + return rootCommand.Parse(args).Invoke(); } - private static Command InstallCommand() => - new( + private static Command InstallCommand() + { + Command installCommand = new( name: "install", description: "Installs SOS and configures LLDB to load it on startup.") { - // Handler - CommandHandler.Create((console, architecture) => InvokeAsync(console, architecture, install: true)), - // Options - ArchitectureOption() + ArchitectureOption }; - private static Option ArchitectureOption() => - new( - aliases: new[] { "-a", "--arch", "--architecture" }, - description: "The processor architecture to install.") + installCommand.SetAction(parseResult => Invoke( + parseResult.Configuration.Output, + parseResult.Configuration.Error, + architecture: parseResult.GetValue(ArchitectureOption), + install: true)); + + return installCommand; + } + + private static readonly Option ArchitectureOption = + new("--architecture", "-a", "--arch") { - Argument = new Argument(name: "architecture") + Description = "The processor architecture to install." }; - private static Command UninstallCommand() => - new( + private static Command UninstallCommand() + { + Command uninstallCommand = new( name: "uninstall", - description: "Uninstalls SOS and reverts any configuration changes to LLDB.") - { - Handler = CommandHandler.Create((console) => InvokeAsync(console, architecture: null, install: false)) - }; + description: "Uninstalls SOS and reverts any configuration changes to LLDB."); + + uninstallCommand.SetAction(parseResult => Invoke( + parseResult.Configuration.Output, + parseResult.Configuration.Error, + architecture: null, + install: false)); + + return uninstallCommand; + } - private static Task InvokeAsync(IConsole console, Architecture? architecture, bool install) + private static int Invoke(TextWriter stdOut, TextWriter stdError, Architecture? architecture, bool install) { try { - InstallHelper sosInstaller = new((message) => console.Out.WriteLine(message), architecture); + InstallHelper sosInstaller = new((message) => stdOut.WriteLine(message), architecture); if (install) { sosInstaller.Install(); @@ -69,10 +77,10 @@ private static Task InvokeAsync(IConsole console, Architecture? architectur } catch (SOSInstallerException ex) { - console.Error.WriteLine(ex.Message); - return Task.FromResult(1); + stdError.WriteLine(ex.Message); + return 1; } - return Task.FromResult(0); + return 0; } } } diff --git a/src/Tools/dotnet-sos/dotnet-sos.csproj b/src/Tools/dotnet-sos/dotnet-sos.csproj index 949eb8e557..e20f833a7f 100644 --- a/src/Tools/dotnet-sos/dotnet-sos.csproj +++ b/src/Tools/dotnet-sos/dotnet-sos.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppMinTargetFramework) dotnet-sos @@ -8,9 +8,6 @@ $(Description) tools/$(TargetFramework)/any - - - diff --git a/src/Tools/dotnet-stack/Program.cs b/src/Tools/dotnet-stack/Program.cs index fe208f761b..7d06dd9ecb 100644 --- a/src/Tools/dotnet-stack/Program.cs +++ b/src/Tools/dotnet-stack/Program.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. -using System.CommandLine.Builder; -using System.CommandLine.Parsing; +using System.CommandLine; using System.Threading.Tasks; using Microsoft.Internal.Common; using Microsoft.Internal.Common.Commands; @@ -13,14 +12,14 @@ internal static class Program { public static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(ReportCommandHandler.ReportCommand()) - .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that stack traces can be collected from.")) - .AddCommand(SymbolicateHandler.SymbolicateCommand()) - .UseToolsDefaults() - .Build(); + RootCommand rootCommand = new() + { + ReportCommandHandler.ReportCommand(), + ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that stack traces can be collected from."), + SymbolicateHandler.SymbolicateCommand() + }; - return parser.InvokeAsync(args); + return rootCommand.Parse(args).InvokeAsync(); } } } diff --git a/src/Tools/dotnet-stack/ReportCommand.cs b/src/Tools/dotnet-stack/ReportCommand.cs index 645de24def..c8f2ecc365 100644 --- a/src/Tools/dotnet-stack/ReportCommand.cs +++ b/src/Tools/dotnet-stack/ReportCommand.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.IO; using System.Diagnostics.Tracing; using System.IO; using System.Threading; @@ -22,18 +20,15 @@ namespace Microsoft.Diagnostics.Tools.Stack { internal static class ReportCommandHandler { - private delegate Task ReportDelegate(CancellationToken ct, IConsole console, int processId, string name, TimeSpan duration); - /// /// Reports a stack trace /// /// The cancellation token - /// /// The process to report the stack from. /// The name of process to report the stack from. /// The duration of to trace the target for. /// - private static async Task Report(CancellationToken ct, IConsole console, int processId, string name, TimeSpan duration) + private static async Task Report(CancellationToken ct, TextWriter stdOutput, TextWriter stdError, int processId, string name, TimeSpan duration) { string tempNetTraceFilename = Path.Join(Path.GetTempPath(), Path.GetRandomFileName() + ".nettrace"); string tempEtlxFilename = ""; @@ -57,12 +52,12 @@ private static async Task Report(CancellationToken ct, IConsole console, in if (processId < 0) { - console.Error.WriteLine("Process ID should not be negative."); + stdError.WriteLine("Process ID should not be negative."); return -1; } else if (processId == 0) { - console.Error.WriteLine("--process-id is required"); + stdError.WriteLine("--process-id is required"); return -1; } @@ -91,7 +86,7 @@ private static async Task Report(CancellationToken ct, IConsole console, in Task completedTask = await Task.WhenAny(copyTask, timeoutTask).ConfigureAwait(false); if (completedTask == timeoutTask) { - console.Out.WriteLine($"# Sufficiently large applications can cause this command to take non-trivial amounts of time"); + stdOutput.WriteLine($"# Sufficiently large applications can cause this reportCommand to take non-trivial amounts of time"); } await copyTask.ConfigureAwait(false); } @@ -142,9 +137,9 @@ private static async Task Report(CancellationToken ct, IConsole console, in foreach ((int threadId, List samples) in samplesForThread) { #if DEBUG - console.Out.WriteLine($"Found {samples.Count} stacks for thread 0x{threadId:X}"); + stdOutput.WriteLine($"Found {samples.Count} stacks for thread 0x{threadId:X}"); #endif - PrintStack(console, threadId, samples[0], stackSource); + PrintStack(stdOutput, threadId, samples[0], stackSource); } } } @@ -154,7 +149,7 @@ private static async Task Report(CancellationToken ct, IConsole console, in } catch (Exception ex) { - Console.Error.WriteLine($"[ERROR] {ex}"); + stdError.WriteLine($"[ERROR] {ex}"); return -1; } finally @@ -173,55 +168,58 @@ private static async Task Report(CancellationToken ct, IConsole console, in return 0; } - private static void PrintStack(IConsole console, int threadId, StackSourceSample stackSourceSample, StackSource stackSource) + private static void PrintStack(TextWriter stdOutput, int threadId, StackSourceSample stackSourceSample, StackSource stackSource) { - console.Out.WriteLine($"Thread (0x{threadId:X}):"); + stdOutput.WriteLine($"Thread (0x{threadId:X}):"); StackSourceCallStackIndex stackIndex = stackSourceSample.StackIndex; while (!stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), verboseName: false).StartsWith("Thread (")) { - console.Out.WriteLine($" {stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), verboseName: false)}" + stdOutput.WriteLine($" {stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), verboseName: false)}" .Replace("UNMANAGED_CODE_TIME", "[Native Frames]")); stackIndex = stackSource.GetCallerIndex(stackIndex); } - console.Out.WriteLine(); + stdOutput.WriteLine(); } - public static Command ReportCommand() => - new( + public static Command ReportCommand() + { + Command reportCommand = new( name: "report", description: "reports the managed stacks from a running .NET process") { - // Handler - HandlerDescriptor.FromDelegate((ReportDelegate)Report).GetCommandHandler(), - // Options - ProcessIdOption(), - NameOption(), - DurationOption() + ProcessIdOption, + NameOption, + DurationOption }; - private static Option DurationOption() => - new( - alias: "--duration", - description: @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss.") + reportCommand.SetAction((parseResult, ct) => Report(ct, + stdOutput: parseResult.Configuration.Output, + stdError: parseResult.Configuration.Error, + processId: parseResult.GetValue(ProcessIdOption), + name: parseResult.GetValue(NameOption), + duration: parseResult.GetValue(DurationOption))); + + return reportCommand; + } + + private static readonly Option DurationOption = + new("--duration") { - Argument = new Argument(name: "duration-timespan", getDefaultValue: () => TimeSpan.FromMilliseconds(10)), - IsHidden = true + Description = @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss.", + DefaultValueFactory = _ => TimeSpan.FromMilliseconds(10), + Hidden = true }; - public static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id to report the stack.") + public static readonly Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid") + Description = "The process id to report the stack." }; - public static Option NameOption() => - new( - aliases: new[] { "-n", "--name" }, - description: "The name of the process to report the stack.") + public static readonly Option NameOption = + new("--name", "-n") { - Argument = new Argument(name: "name") + Description = "The name of the process to report the stack." }; } } diff --git a/src/Tools/dotnet-stack/Symbolicate.cs b/src/Tools/dotnet-stack/Symbolicate.cs index f78aa6a9c4..ce8b10e59b 100644 --- a/src/Tools/dotnet-stack/Symbolicate.cs +++ b/src/Tools/dotnet-stack/Symbolicate.cs @@ -4,14 +4,13 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; -using System.CommandLine.IO; using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Microsoft.Internal.Common; namespace Microsoft.Diagnostics.Tools.Stack @@ -22,33 +21,32 @@ internal static partial class SymbolicateHandler private static readonly Dictionary s_assemblyFilePathDictionary = new(); private static readonly Dictionary s_metadataReaderDictionary = new(); - private delegate void SymbolicateDelegate(IConsole console, FileInfo inputPath, DirectoryInfo[] searchDir, FileInfo output, bool stdout); - /// /// Get the line number from the Method Token and IL Offset in a stacktrace /// - /// /// Path to the stacktrace text file /// Path of multiple directories with assembly and pdb where the exception occurred /// Output directly to a file /// Output directly to a console - private static void Symbolicate(IConsole console, FileInfo inputPath, DirectoryInfo[] searchDir, FileInfo output, bool stdout) + private static int Symbolicate(TextWriter stdOutput, TextWriter stdError, FileInfo inputPath, DirectoryInfo[] searchDir, FileInfo output, bool stdout) { try { output ??= new FileInfo(inputPath.FullName + ".symbolicated"); - SetAssemblyFilePathDictionary(console, searchDir); + SetAssemblyFilePathDictionary(stdError, searchDir); - CreateSymbolicateFile(console, inputPath.FullName, output.FullName, stdout); + CreateSymbolicateFile(stdOutput, stdError, inputPath.FullName, output.FullName, stdout); + return 0; } catch (Exception e) { - console.Error.WriteLine(e.Message); + stdError.WriteLine(e.Message); + return 1; } } - private static void SetAssemblyFilePathDictionary(IConsole console, DirectoryInfo[] searchDir) + private static void SetAssemblyFilePathDictionary(TextWriter stdError, DirectoryInfo[] searchDir) { try { @@ -102,7 +100,7 @@ private static void SetAssemblyFilePathDictionary(IConsole console, DirectoryInf } catch (Exception e) { - console.Error.WriteLine(e.Message); + stdError.WriteLine(e.Message); } } @@ -126,7 +124,7 @@ private static List GrabFiles(List paths, string searchPattern) } } - private static void CreateSymbolicateFile(IConsole console, string inputPath, string outputPath, bool isStdout) + private static void CreateSymbolicateFile(TextWriter stdOutput, TextWriter stdError, string inputPath, string outputPath, bool isStdout) { try { @@ -138,14 +136,14 @@ private static void CreateSymbolicateFile(IConsole console, string inputPath, st fileStreamWriter?.WriteLine(ret); if (isStdout) { - console.Out.WriteLine(ret); + stdOutput.WriteLine(ret); } } - console.Out.WriteLine($"\nOutput: {outputPath}\n"); + stdOutput.WriteLine($"\nOutput: {outputPath}\n"); } catch (Exception e) { - console.Error.WriteLine(e.Message); + stdError.WriteLine(e.Message); } } @@ -295,46 +293,56 @@ private static string GetLineFromMetadata(MetadataReader reader, string line, St } } - public static Command SymbolicateCommand() => - new( - name: "symbolicate", description: "Get the line number from the Method Token and IL Offset in a stacktrace") + public static Command SymbolicateCommand() + { + Command symbolicateCommand = new( + name: "symbolicate", + description: "Get the line number from the Method Token and IL Offset in a stacktrace") { - // Handler - HandlerDescriptor.FromDelegate((SymbolicateDelegate)Symbolicate).GetCommandHandler(), - // Arguments and Options - InputFileArgument(), - SearchDirectoryOption(), - OutputFileOption(), - StandardOutOption() + InputFileArgument, + SearchDirectoryOption, + OutputFileOption, + StandardOutOption }; - public static Argument InputFileArgument() => - new Argument(name: "input-path") + symbolicateCommand.SetAction((parseResult, ct) => Task.FromResult(Symbolicate( + stdOutput: parseResult.Configuration.Output, + stdError: parseResult.Configuration.Error, + inputPath: parseResult.GetValue(InputFileArgument), + searchDir: parseResult.GetValue(SearchDirectoryOption), + output: parseResult.GetValue(OutputFileOption), + stdout: parseResult.GetValue(StandardOutOption)))); + + return symbolicateCommand; + } + + public static readonly Argument InputFileArgument = + new Argument("input-path") { Description = "Path to the stacktrace text file", Arity = ArgumentArity.ExactlyOne - }.ExistingOnly(); + }.AcceptExistingOnly(); - public static Option SearchDirectoryOption() => - new(new[] { "-d", "--search-dir" }, "Path of multiple directories with assembly and pdb") + public static readonly Option SearchDirectoryOption = + new Option("--search-dir", "-d") { - Argument = new Argument(name: "directory1 directory2 ...", getDefaultValue: () => new DirectoryInfo(Directory.GetCurrentDirectory()).GetDirectories()) - { - Arity = ArgumentArity.ZeroOrMore - }.ExistingOnly() - }; + Description = "Path of multiple directories with assembly and pdb", + DefaultValueFactory = _ => new DirectoryInfo(Directory.GetCurrentDirectory()).GetDirectories(), + Arity = ArgumentArity.ZeroOrMore + }.AcceptExistingOnly(); - public static Option OutputFileOption() => - new(new[] { "-o", "--output" }, "Output directly to a file (Default: .symbolicated)") + public static readonly Option OutputFileOption = + new("--output", "-o") { - Argument = new Argument(name: "output-path") - { - Arity = ArgumentArity.ZeroOrOne - } + Description = "Output directly to a file (Default: .symbolicated)", + Arity = ArgumentArity.ZeroOrOne }; - public static Option StandardOutOption() => - new(new[] { "-c", "--stdout" }, getDefaultValue: () => false, "Output directly to a console"); + public static readonly Option StandardOutOption = + new("--stdout", "-c") + { + Description = "Output directly to a console" + }; [GeneratedRegex(@" at (?[\w+\.?]+)\.(?\w+)\((?.*)\) in (?[\w+\.?]+):token (?0x\d+)\+(?0x\d+)", RegexOptions.Compiled)] private static partial Regex GetSymbolRegex(); diff --git a/src/Tools/dotnet-stack/dotnet-stack.csproj b/src/Tools/dotnet-stack/dotnet-stack.csproj index cd7a031f3c..96010bad44 100644 --- a/src/Tools/dotnet-stack/dotnet-stack.csproj +++ b/src/Tools/dotnet-stack/dotnet-stack.csproj @@ -12,7 +12,6 @@ - @@ -21,7 +20,6 @@ - diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index f1f40b384b..77853ee4b1 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; using System.CommandLine.Rendering; using System.Diagnostics; using System.IO; @@ -31,8 +30,6 @@ private static void ConsoleWriteLine(string str) } } - private delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string port, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown); - /// /// Collects a diagnostic trace from a currently running process or launch a child process and trace it. /// Append -- to the collect command to instruct the tool to run a command and trace it immediately. By default the IO from this process is hidden, but the --show-child-io option may be used to show the child process IO. @@ -57,7 +54,7 @@ private static void ConsoleWriteLine(string str) /// A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set. /// Collect rundown events. /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown) + private static async Task Collect(CancellationToken ct, CommandLineConfiguration cliConfig, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown) { bool collectionStopped = false; bool cancelOnEnter = true; @@ -228,7 +225,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i // if builder returned null, it means we received ctrl+C while waiting for clients to connect. Exit gracefully. if (holder == null) { - return (int)await Task.FromResult(ReturnCode.Ok).ConfigureAwait(false); + return (int)ReturnCode.Ok; } diagnosticsClient = holder.Client; if (ProcessLauncher.Launcher.HasChildProc) @@ -470,7 +467,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (format != TraceFileFormat.NetTrace) { string outputFilename = TraceFileFormatConverter.GetConvertedFilename(output.FullName, outputfile: null, format); - TraceFileFormatConverter.ConvertToFormat(console, format, fileToConvert: output.FullName, outputFilename); + TraceFileFormatConverter.ConvertToFormat(cliConfig.Output, cliConfig.Error, format, fileToConvert: output.FullName, outputFilename); } } @@ -504,7 +501,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i { if (printStatusOverTime) { - if (console.GetTerminal() != null) + if (!Console.IsOutputRedirected) { Console.CursorVisible = true; } @@ -519,7 +516,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i } ProcessLauncher.Launcher.Cleanup(); } - return await Task.FromResult(ret).ConfigureAwait(false); + return ret; } private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) @@ -556,151 +553,153 @@ private static string GetSize(long length) } } - public static Command CollectCommand() => - new( - name: "collect", - description: "Collects a diagnostic trace from a currently running process or launch a child process and trace it. Append -- to the collect command to instruct the tool to run a command and trace it immediately. When tracing a child process, the exit code of dotnet-trace shall be that of the traced process unless the trace process encounters an error.") + public static Command CollectCommand() + { + Command collectCommand = new("collect") { - // Handler - HandlerDescriptor.FromDelegate((CollectDelegate)Collect).GetCommandHandler(), // Options - CommonOptions.ProcessIdOption(), - CircularBufferOption(), - OutputPathOption(), - ProvidersOption(), - ProfileOption(), - CommonOptions.FormatOption(), - DurationOption(), - CLREventsOption(), - CLREventLevelOption(), - CommonOptions.NameOption(), - DiagnosticPortOption(), - ShowChildIOOption(), - ResumeRuntimeOption(), - StoppingEventProviderNameOption(), - StoppingEventEventNameOption(), - StoppingEventPayloadFilterOption(), - RundownOption() + CommonOptions.ProcessIdOption, + CircularBufferOption, + OutputPathOption, + ProvidersOption, + ProfileOption, + CommonOptions.FormatOption, + DurationOption, + CLREventsOption, + CLREventLevelOption, + CommonOptions.NameOption, + DiagnosticPortOption, + ShowChildIOOption, + ResumeRuntimeOption, + StoppingEventProviderNameOption, + StoppingEventEventNameOption, + StoppingEventPayloadFilterOption, + RundownOption }; + collectCommand.TreatUnmatchedTokensAsErrors = false; // see the logic in Program.Main that handles UnmatchedTokens + collectCommand.Description = "Collects a diagnostic trace from a currently running process or launch a child process and trace it. Append -- to the collect command to instruct the tool to run a command and trace it immediately. When tracing a child process, the exit code of dotnet-trace shall be that of the traced process unless the trace process encounters an error."; + + collectCommand.SetAction((parseResult, ct) => Collect( + ct, + cliConfig: parseResult.Configuration, + processId: parseResult.GetValue(CommonOptions.ProcessIdOption), + output: parseResult.GetValue(OutputPathOption), + buffersize: parseResult.GetValue(CircularBufferOption), + providers: parseResult.GetValue(ProvidersOption) ?? string.Empty, + profile: parseResult.GetValue(ProfileOption) ?? string.Empty, + format: parseResult.GetValue(CommonOptions.FormatOption), + duration: parseResult.GetValue(DurationOption), + clrevents: parseResult.GetValue(CLREventsOption) ?? string.Empty, + clreventlevel: parseResult.GetValue(CLREventLevelOption) ?? string.Empty, + name: parseResult.GetValue(CommonOptions.NameOption), + diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty, + showchildio: parseResult.GetValue(ShowChildIOOption), + resumeRuntime: parseResult.GetValue(ResumeRuntimeOption), + stoppingEventProviderName: parseResult.GetValue(StoppingEventProviderNameOption), + stoppingEventEventName: parseResult.GetValue(StoppingEventEventNameOption), + stoppingEventPayloadFilter: parseResult.GetValue(StoppingEventPayloadFilterOption), + rundown: parseResult.GetValue(RundownOption))); + + return collectCommand; + } - private static uint DefaultCircularBufferSizeInMB() => 256; + private const uint DefaultCircularBufferSizeInMB = 256; - private static Option CircularBufferOption() => - new( - alias: "--buffersize", - description: $"Sets the size of the in-memory circular buffer in megabytes. Default {DefaultCircularBufferSizeInMB()} MB.") + private static readonly Option CircularBufferOption = + new("--buffersize") { - Argument = new Argument(name: "size", getDefaultValue: DefaultCircularBufferSizeInMB) + Description = $"Sets the size of the in-memory circular buffer in megabytes. Default {DefaultCircularBufferSizeInMB} MB.", + DefaultValueFactory = _ => DefaultCircularBufferSizeInMB, }; public static string DefaultTraceName => "default"; - private static Option OutputPathOption() => - new( - aliases: new[] { "-o", "--output" }, - description: $"The output path for the collected trace data. If not specified it defaults to '__.nettrace', e.g., 'myapp_20210315_111514.nettrace'.") + private static readonly Option OutputPathOption = + new("--output", "-o") { - Argument = new Argument(name: "trace-file-path", getDefaultValue: () => new FileInfo(DefaultTraceName)) + Description = $"The output path for the collected trace data. If not specified it defaults to '__.nettrace', e.g., 'myapp_20210315_111514.nettrace'.", + DefaultValueFactory = _ => new FileInfo(DefaultTraceName) }; - private static Option ProvidersOption() => - new( - alias: "--providers", - description: @"A comma delimitted list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]'," + + private static readonly Option ProvidersOption = + new("--providers") + { + Description = @"A comma delimitted list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]'," + @"where Provider is in the form: 'KnownProviderName[:[Flags][:[Level][:[KeyValueArgs]]]]', and KeyValueArgs is in the form: " + @"'[key1=value1][;key2=value2]'. Values in KeyValueArgs that contain ';' or '=' characters need to be surrounded by '""', " + @"e.g., FilterAndPayloadSpecs=""MyProvider/MyEvent:-Prop1=Prop1;Prop2=Prop2.A.B;"". Depending on your shell, you may need to " + @"escape the '""' characters and/or surround the entire provider specification in quotes, e.g., " + @"--providers 'KnownProviderName:0x1:1:FilterSpec=\""KnownProviderName/EventName:-Prop1=Prop1;Prop2=Prop2.A.B;\""'. These providers are in " + @"addition to any providers implied by the --profile argument. If there is any discrepancy for a particular provider, the " + - @"configuration here takes precedence over the implicit configuration from the profile. See documentation for examples.") - { - Argument = new Argument(name: "list-of-comma-separated-providers", getDefaultValue: () => string.Empty) // TODO: Can we specify an actual type? + @"configuration here takes precedence over the implicit configuration from the profile. See documentation for examples." + // TODO: Can we specify an actual type? }; - private static Option ProfileOption() => - new( - alias: "--profile", - description: @"A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.") + private static readonly Option ProfileOption = + new("--profile") { - Argument = new Argument(name: "profile-name", getDefaultValue: () => string.Empty) + Description = @"A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly." }; - private static Option DurationOption() => - new( - alias: "--duration", - description: @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss.") + private static readonly Option DurationOption = + new("--duration") { - Argument = new Argument(name: "duration-timespan", getDefaultValue: () => default) + Description = @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss." }; - private static Option CLREventsOption() => - new( - alias: "--clrevents", - description: @"List of CLR runtime events to emit.") + private static readonly Option CLREventsOption = + new("--clrevents") { - Argument = new Argument(name: "clrevents", getDefaultValue: () => string.Empty) + Description = @"List of CLR runtime events to emit." }; - private static Option CLREventLevelOption() => - new( - alias: "--clreventlevel", - description: @"Verbosity of CLR events to be emitted.") + private static readonly Option CLREventLevelOption = + new("--clreventlevel") { - Argument = new Argument(name: "clreventlevel", getDefaultValue: () => string.Empty) + Description = @"Verbosity of CLR events to be emitted." }; - private static Option DiagnosticPortOption() => - new( - aliases: new[] { "--dport", "--diagnostic-port" }, - description: @"The path to a diagnostic port to be used.") + + private static readonly Option DiagnosticPortOption = + new("--diagnostic-port", "--dport") { - Argument = new Argument(name: "diagnosticPort", getDefaultValue: () => string.Empty) + Description = @"The path to a diagnostic port to be used." }; - private static Option ShowChildIOOption() => - new( - alias: "--show-child-io", - description: @"Shows the input and output streams of a launched child process in the current console.") + + private static readonly Option ShowChildIOOption = + new("--show-child-io") { - Argument = new Argument(name: "show-child-io", getDefaultValue: () => false) + Description = @"Shows the input and output streams of a launched child process in the current console." }; - private static Option ResumeRuntimeOption() => - new( - alias: "--resume-runtime", - description: @"Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false") + private static readonly Option ResumeRuntimeOption = + new("--resume-runtime") { - Argument = new Argument(name: "resumeRuntime", getDefaultValue: () => true) + Description = @"Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false", + DefaultValueFactory = _ => true, }; - private static Option StoppingEventProviderNameOption() => - new( - alias: "--stopping-event-provider-name", - description: @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching provider name. For a more specific stopping event, additionally provide `--stopping-event-event-name` and/or `--stopping-event-payload-filter`.") + private static readonly Option StoppingEventProviderNameOption = + new("--stopping-event-provider-name") { - Argument = new Argument(name: "stoppingEventProviderName", getDefaultValue: () => null) + Description = @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching provider name. For a more specific stopping event, additionally provide `--stopping-event-event-name` and/or `--stopping-event-payload-filter`." }; - private static Option StoppingEventEventNameOption() => - new( - alias: "--stopping-event-event-name", - description: @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching event name. Requires `--stopping-event-provider-name` to be set. For a more specific stopping event, additionally provide `--stopping-event-payload-filter`.") + private static readonly Option StoppingEventEventNameOption = + new("--stopping-event-event-name") { - Argument = new Argument(name: "stoppingEventEventName", getDefaultValue: () => null) + Description = @"A string, parsed as-is, that will stop the trace upon hitting an event with the matching event name. Requires `--stopping-event-provider-name` to be set. For a more specific stopping event, additionally provide `--stopping-event-payload-filter`." }; - private static Option StoppingEventPayloadFilterOption() => - new( - alias: "--stopping-event-payload-filter", - description: @"A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set.") + private static readonly Option StoppingEventPayloadFilterOption = + new("--stopping-event-payload-filter") { - Argument = new Argument(name: "stoppingEventPayloadFilter", getDefaultValue: () => null) + Description = @"A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set." }; - private static Option RundownOption() => - new( - alias: "--rundown", - description: @"Collect rundown events unless specified false.") + + private static readonly Option RundownOption = + new("--rundown") { - Argument = new Argument(name: "rundown") + Description = @"Collect rundown events unless specified false." }; } } diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs index 75838ef29b..055051ae5a 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.IO; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.Internal.Common; namespace Microsoft.Diagnostics.Tools.Trace @@ -16,15 +16,15 @@ internal static class ConvertCommandHandler // The first 8 bytes of a nettrace file are the ASCII string "Nettrace" private static readonly byte[] NetTraceHeader = [0x4E, 0x65, 0x74, 0x74, 0x72, 0x61, 0x63, 0x65]; - public static int ConvertFile(IConsole console, FileInfo inputFilename, TraceFileFormat format, FileInfo output) + public static int ConvertFile(TextWriter stdOut, TextWriter stdError, FileInfo inputFilename, TraceFileFormat format, FileInfo output) { if (!Enum.IsDefined(format)) { - console.Error.WriteLine($"Please specify a valid option for the --format. Valid options are: {string.Join(", ", Enum.GetNames())}."); + stdError.WriteLine($"Please specify a valid option for the --format. Valid options are: {string.Join(", ", Enum.GetNames())}."); return ErrorCodes.ArgumentError; } - if (!ValidateNetTraceHeader(console, inputFilename.FullName)) + if (!ValidateNetTraceHeader(stdError, inputFilename.FullName)) { return ErrorCodes.ArgumentError; } @@ -33,13 +33,13 @@ public static int ConvertFile(IConsole console, FileInfo inputFilename, TraceFil if (format != TraceFileFormat.NetTrace) { - TraceFileFormatConverter.ConvertToFormat(console, format, inputFilename.FullName, outputFilename); + TraceFileFormatConverter.ConvertToFormat(stdOut, stdError, format, inputFilename.FullName, outputFilename); return 0; } - return CopyNetTrace(console, inputFilename.FullName, outputFilename); + return CopyNetTrace(stdOut, stdError, inputFilename.FullName, outputFilename); - static bool ValidateNetTraceHeader(IConsole console, string filename) + static bool ValidateNetTraceHeader(TextWriter stdError, string filename) { try { @@ -54,35 +54,35 @@ static bool ValidateNetTraceHeader(IConsole console, string filename) if (readBuffer.Length != 0 || !header.SequenceEqual(NetTraceHeader)) { - console.Error.WriteLine($"'{filename}' is not a valid nettrace file."); + stdError.WriteLine($"'{filename}' is not a valid nettrace file."); return false; } } catch (Exception ex) { - console.Error.WriteLine($"Error reading '{filename}': {ex.Message}"); + stdError.WriteLine($"Error reading '{filename}': {ex.Message}"); return false; } return true; } - static int CopyNetTrace(IConsole console, string inputfile, string outputfile) + static int CopyNetTrace(TextWriter stdOut, TextWriter stdError, string inputfile, string outputfile) { if (inputfile == outputfile) { - console.Error.WriteLine("Input and output filenames are the same. Skipping copy."); + stdError.WriteLine("Input and output filenames are the same. Skipping copy."); return 0; } - console.Out.WriteLine($"Copying nettrace to:\t{outputfile}"); + stdOut.WriteLine($"Copying nettrace to:\t{outputfile}"); try { File.Copy(inputfile, outputfile); } catch (Exception ex) { - console.Error.WriteLine($"Error copying nettrace to {outputfile}: {ex.Message}"); + stdError.WriteLine($"Error copying nettrace to {outputfile}: {ex.Message}"); return ErrorCodes.UnknownError; } @@ -90,31 +90,40 @@ static int CopyNetTrace(IConsole console, string inputfile, string outputfile) } } - public static Command ConvertCommand() => - new( + public static Command ConvertCommand() + { + Command convertCommand = new( name: "convert", description: "Converts traces to alternate formats for use with alternate trace analysis tools. Can only convert from the nettrace format") { - // Handler - System.CommandLine.Invocation.CommandHandler.Create(ConvertFile), // Arguments and Options - InputFileArgument(), - CommonOptions.ConvertFormatOption(), - OutputOption(), + InputFileArgument, + CommonOptions.ConvertFormatOption, + OutputOption, }; - private static Argument InputFileArgument() => - new Argument(name: "input-filename", getDefaultValue: () => new FileInfo(CollectCommandHandler.DefaultTraceName)) + convertCommand.SetAction((parseResult, ct) => Task.FromResult(ConvertFile( + stdOut: parseResult.Configuration.Output, + stdError: parseResult.Configuration.Error, + inputFilename: parseResult.GetValue(InputFileArgument), + format: parseResult.GetValue(CommonOptions.ConvertFormatOption), + output: parseResult.GetValue(OutputOption + )))); + + return convertCommand; + } + + private static readonly Argument InputFileArgument = + new Argument(name: "input-filename") { - Description = $"Input trace file to be converted. Defaults to '{CollectCommandHandler.DefaultTraceName}'." - }.ExistingOnly(); + Description = $"Input trace file to be converted. Defaults to '{CollectCommandHandler.DefaultTraceName}'.", + DefaultValueFactory = _ => new FileInfo(CollectCommandHandler.DefaultTraceName), + }.AcceptExistingOnly(); - private static Option OutputOption() => - new( - aliases: new[] { "-o", "--output" }, - description: "Output filename. Extension of target format will be added.") + private static readonly Option OutputOption = + new("--output", "-o") { - Argument = new Argument(name: "output-filename") + Description = "Output filename. Extension of target format will be added.", }; } } diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index a25f863245..7e8c2cea56 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -14,7 +14,7 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal sealed class ListProfilesCommandHandler { - public static async Task GetProfiles(IConsole console) + public static int GetProfiles() { try { @@ -23,7 +23,6 @@ public static async Task GetProfiles(IConsole console) Console.Out.WriteLine($"\t{profile.Name,-16} - {profile.Description}"); } - await Task.FromResult(0).ConfigureAwait(false); return 0; } catch (Exception ex) @@ -33,13 +32,15 @@ public static async Task GetProfiles(IConsole console) } } - public static Command ListProfilesCommand() => - new( + public static Command ListProfilesCommand() + { + Command listProfilesCommand = new( name: "list-profiles", - description: "Lists pre-built tracing profiles with a description of what providers and filters are in each profile") - { - Handler = CommandHandler.Create(GetProfiles), - }; + description: "Lists pre-built tracing profiles with a description of what providers and filters are in each profile"); + + listProfilesCommand.SetAction((parseResult, ct) => Task.FromResult(GetProfiles())); + return listProfilesCommand; + } internal static IEnumerable DotNETRuntimeProfiles { get; } = new[] { new Profile( diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs index 87b7e8cade..7330cc1604 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Binding; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -30,16 +30,14 @@ public static List ByIDSortedInclusiveMetric(this CallTree cal return ret; } - private delegate Task ReportDelegate(CancellationToken ct, IConsole console, string traceFile); - private static Task Report(CancellationToken ct, IConsole console, string traceFile) + private static Task Report() { Console.Error.WriteLine("Error: subcommand was not provided. Available subcommands:"); Console.Error.WriteLine(" topN: Finds the top N methods on the callstack the longest."); return Task.FromResult(-1); } - private delegate Task TopNReportDelegate(CancellationToken ct, IConsole console, string traceFile, int n, bool inclusive, bool verbose); - private static async Task TopNReport(CancellationToken ct, IConsole console, string traceFile, int number, bool inclusive, bool verbose) + private static int TopNReport(string traceFile, int number, bool inclusive, bool verbose) { try { @@ -92,69 +90,69 @@ private static async Task TopNReport(CancellationToken ct, IConsole console PrintReportHelper.TopNWriteToStdOut(nodesToReport, inclusive, verbose); } - return await Task.FromResult(0).ConfigureAwait(false); + return 0; } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex}"); + return 1; } - - return await Task.FromResult(0).ConfigureAwait(false); } - public static Command ReportCommand() => - new( + public static Command ReportCommand() + { + Command topNCommand = new( + name: "topN", + description: "Finds the top N methods that have been on the callstack the longest.") + { + TopNOption, + InclusiveOption, + VerboseOption, + }; + + topNCommand.SetAction((parseResult, ct) => Task.FromResult(TopNReport( + traceFile: parseResult.GetValue(FileNameArgument), + number: parseResult.GetValue(TopNOption), + inclusive: parseResult.GetValue(InclusiveOption), + verbose: parseResult.GetValue(VerboseOption) + ))); + + Command reportCommand = new( name: "report", description: "Generates a report into stdout from a previously generated trace.") { - //Handler - HandlerDescriptor.FromDelegate((ReportDelegate)Report).GetCommandHandler(), - //Options - FileNameArgument(), - new Command( - name: "topN", - description: "Finds the top N methods that have been on the callstack the longest.") - { - //Handler - HandlerDescriptor.FromDelegate((TopNReportDelegate)TopNReport).GetCommandHandler(), - TopNOption(), - InclusiveOption(), - VerboseOption(), - } + FileNameArgument, + topNCommand }; + reportCommand.SetAction((parseResult, ct) => Report()); + + return reportCommand; + } - private static Argument FileNameArgument() => + private static readonly Argument FileNameArgument = new("trace_filename") { - Name = "tracefile", Description = "The file path for the trace being analyzed.", Arity = new ArgumentArity(1, 1) }; - private static Option TopNOption() - { - return new Option( - aliases: new[] { "-n", "--number" }, - description: $"Gives the top N methods on the callstack.") + private static readonly Option TopNOption = + new("--number", "-n") { - Argument = new Argument(name: "n", getDefaultValue: () => 5) + Description = "Gives the top N methods on the callstack.", + DefaultValueFactory = _ => 5 }; - } - private static Option InclusiveOption() => - new( - aliases: new[] { "--inclusive" }, - description: $"Output the top N methods based on inclusive time. If not specified, exclusive time is used by default.") + private static readonly Option InclusiveOption = + new("--inclusive") { - Argument = new Argument(name: "inclusive", getDefaultValue: () => false) + Description = "Output the top N methods based on inclusive time. If not specified, exclusive time is used by default." }; - private static Option VerboseOption() => - new( - aliases: new[] { "-v", "--verbose" }, - description: $"Output the parameters of each method in full. If not specified, parameters will be truncated.") + private static readonly Option VerboseOption = + new("--verbose", "-v") { - Argument = new Argument(name: "verbose", getDefaultValue: () => false) + Description = "Output the parameters of each method in full. If not specified, parameters will be truncated." }; } } diff --git a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs index a5e9fd7ad2..98be9c055a 100644 --- a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs +++ b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs @@ -7,39 +7,32 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal static class CommonOptions { - public static Option ProcessIdOption() => - new( - aliases: new[] { "-p", "--process-id" }, - description: "The process id to collect the trace.") + public static readonly Option ProcessIdOption = + new("--process-id", "-p") { - Argument = new Argument(name: "pid") + Description = "The process id to collect the trace." }; - public static Option NameOption() => - new( - aliases: new[] { "-n", "--name" }, - description: "The name of the process to collect the trace.") + public static readonly Option NameOption = + new("--name", "-n") { - Argument = new Argument(name: "name") + Description = "The name of the process to collect the trace.", }; public static TraceFileFormat DefaultTraceFileFormat() => TraceFileFormat.NetTrace; - public static Option FormatOption() => - new( - alias: "--format", - description: $"If not using the default NetTrace format, an additional file will be emitted with the specified format under the same output name and with the corresponding format extension. The default format is {DefaultTraceFileFormat()}.") + public static readonly Option FormatOption = + new("--format") { - Argument = new Argument(name: "trace-file-format", getDefaultValue: DefaultTraceFileFormat) + Description = $"If not using the default NetTrace format, an additional file will be emitted with the specified format under the same output name and with the corresponding format extension. The default format is {DefaultTraceFileFormat()}.", + DefaultValueFactory = _ => DefaultTraceFileFormat() }; - public static Option ConvertFormatOption() => - new( - alias: "--format", - description: $"Sets the output format for the trace file conversion.") + public static readonly Option ConvertFormatOption = + new("--format") { - Argument = new Argument(name: "trace-file-format"), - IsRequired = true + Description = $"Sets the output format for the trace file conversion.", + Required = true }; } } diff --git a/src/Tools/dotnet-trace/Program.cs b/src/Tools/dotnet-trace/Program.cs index 797e6073aa..41f34c1580 100644 --- a/src/Tools/dotnet-trace/Program.cs +++ b/src/Tools/dotnet-trace/Program.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.CommandLine.Builder; +using System.CommandLine; using System.CommandLine.Parsing; using System.Threading.Tasks; using Microsoft.Internal.Common; @@ -15,26 +15,33 @@ internal static class Program { public static Task Main(string[] args) { - Parser parser = new CommandLineBuilder() - .AddCommand(CollectCommandHandler.CollectCommand()) - .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that traces can be collected from.")) - .AddCommand(ListProfilesCommandHandler.ListProfilesCommand()) - .AddCommand(ConvertCommandHandler.ConvertCommand()) - .AddCommand(ReportCommandHandler.ReportCommand()) - .UseToolsDefaults() - .Build(); - ParseResult parseResult = parser.Parse(args); + RootCommand rootCommand = new() + { + CollectCommandHandler.CollectCommand(), + ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that traces can be collected from."), + ListProfilesCommandHandler.ListProfilesCommand(), + ConvertCommandHandler.ConvertCommand(), + ReportCommandHandler.ReportCommand() + }; + + CommandLineConfiguration configuration = new(rootCommand) + { + // System.CommandLine should not interfere with Ctrl+C + ProcessTerminationTimeout = null + }; + + ParseResult parseResult = rootCommand.Parse(args, configuration); string parsedCommandName = parseResult.CommandResult.Command.Name; if (parsedCommandName == "collect") { - IReadOnlyCollection unparsedTokens = parseResult.UnparsedTokens; + IReadOnlyCollection unparsedTokens = parseResult.UnmatchedTokens; // If we notice there are unparsed tokens, user might want to attach on startup. if (unparsedTokens.Count > 0) { ProcessLauncher.Launcher.PrepareChildProcess(args); } } - return parser.InvokeAsync(args); + return parseResult.InvokeAsync(); } } } diff --git a/src/Tools/dotnet-trace/TraceFileFormatConverter.cs b/src/Tools/dotnet-trace/TraceFileFormatConverter.cs index b954cf0524..d72b4c7d4a 100644 --- a/src/Tools/dotnet-trace/TraceFileFormatConverter.cs +++ b/src/Tools/dotnet-trace/TraceFileFormatConverter.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.IO; using System.IO; using Microsoft.Diagnostics.Symbols; using Microsoft.Diagnostics.Tracing; @@ -34,7 +32,7 @@ internal static string GetConvertedFilename(string fileToConvert, string outputf return Path.ChangeExtension(outputfile, TraceFileFormatExtensions[format]); } - internal static void ConvertToFormat(IConsole console, TraceFileFormat format, string fileToConvert, string outputFilename) + internal static void ConvertToFormat(TextWriter stdOut, TextWriter stdError, TraceFileFormat format, string fileToConvert, string outputFilename) { switch (format) { @@ -42,10 +40,10 @@ internal static void ConvertToFormat(IConsole console, TraceFileFormat format, s break; case TraceFileFormat.Speedscope: case TraceFileFormat.Chromium: - console.Out.WriteLine($"Processing trace data file '{fileToConvert}' to create a new {format} file '{outputFilename}'."); + stdOut.WriteLine($"Processing trace data file '{fileToConvert}' to create a new {format} file '{outputFilename}'."); try { - Convert(console, format, fileToConvert, outputFilename); + Convert(format, fileToConvert, outputFilename); } // TODO: On a broken/truncated trace, the exception we get from TraceEvent is a plain System.Exception type because it gets caught and rethrown inside TraceEvent. // We should probably modify TraceEvent to throw a better exception. @@ -53,12 +51,12 @@ internal static void ConvertToFormat(IConsole console, TraceFileFormat format, s { if (ex.ToString().Contains("Read past end of stream.")) { - console.Out.WriteLine("Detected a potentially broken trace. Continuing with best-efforts to convert the trace, but resulting speedscope file may contain broken stacks as a result."); - Convert(console, format, fileToConvert, outputFilename, continueOnError: true); + stdOut.WriteLine("Detected a potentially broken trace. Continuing with best-efforts to convert the trace, but resulting speedscope file may contain broken stacks as a result."); + Convert(format, fileToConvert, outputFilename, continueOnError: true); } else { - console.Error.WriteLine(ex.ToString()); + stdError.WriteLine(ex.ToString()); } } break; @@ -66,10 +64,10 @@ internal static void ConvertToFormat(IConsole console, TraceFileFormat format, s // Validation happened way before this, so we shoud never reach this... throw new ArgumentException($"Invalid TraceFileFormat \"{format}\""); } - console.Out.WriteLine("Conversion complete"); + stdOut.WriteLine("Conversion complete"); } - private static void Convert(IConsole _, TraceFileFormat format, string fileToConvert, string outputFilename, bool continueOnError = false) + private static void Convert(TraceFileFormat format, string fileToConvert, string outputFilename, bool continueOnError = false) { string etlxFilePath = TraceLog.CreateFromEventPipeDataFile(fileToConvert, null, new TraceLogOptions() { ContinueOnError = continueOnError }); using (SymbolReader symbolReader = new(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath }) diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index c5203e6c02..3ac854aae4 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppMinTargetFramework) Microsoft.Diagnostics.Tools.Trace @@ -12,7 +12,6 @@ - diff --git a/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs b/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs index 93ba9c0b16..a7ab6ae139 100644 --- a/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs +++ b/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.IO; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -198,7 +196,7 @@ private async Task> GetCounterTrace(TestConfiguration con { try { - CounterMonitor monitor = new CounterMonitor(); + CounterMonitor monitor = new CounterMonitor(TextWriter.Null, TextWriter.Null); using CancellationTokenSource source = new CancellationTokenSource(DefaultTimeout); @@ -210,7 +208,6 @@ await monitor.Collect( ct: ct, counter_list: counterList, counters: null, - console: new TestConsole(), processId: testRunner.Pid, refreshInterval: 1, format: exportFormat, @@ -303,34 +300,5 @@ private sealed class MetricComponents public string Tags { get; set; } public CounterTypes CounterType { get; set; } } - - private sealed class TestConsole : IConsole - { - private readonly TestStandardStreamWriter _outWriter; - private readonly TestStandardStreamWriter _errorWriter; - - private sealed class TestStandardStreamWriter : IStandardStreamWriter - { - private StringWriter _writer = new(); - public void Write(string value) => _writer.Write(value); - public void WriteLine(string value) => _writer.WriteLine(value); - } - - public TestConsole() - { - _outWriter = new TestStandardStreamWriter(); - _errorWriter = new TestStandardStreamWriter(); - } - - public IStandardStreamWriter Out => _outWriter; - - public bool IsOutputRedirected => true; - - public IStandardStreamWriter Error => _errorWriter; - - public bool IsErrorRedirected => true; - - public bool IsInputRedirected => false; - } } } diff --git a/src/tests/dotnet-counters/CounterMonitorTests.cs b/src/tests/dotnet-counters/CounterMonitorTests.cs index 0f6829db73..d3a5b5840b 100644 --- a/src/tests/dotnet-counters/CounterMonitorTests.cs +++ b/src/tests/dotnet-counters/CounterMonitorTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.Tools; @@ -18,7 +19,6 @@ public class CounterMonitorTests [Fact] public void GenerateCounterListTestSingleProvider() { - CounterMonitor monitor = new(); List counters = CounterMonitor.ParseProviderList("MySource"); Assert.Single(counters); EventPipeCounterGroup mySourceGroup = counters.First(); @@ -29,7 +29,6 @@ public void GenerateCounterListTestSingleProvider() [Fact] public void GenerateCounterListTestSingleProviderWithFilter() { - CounterMonitor monitor = new(); List counters = CounterMonitor.ParseProviderList("MySource[counter1,counter2,counter3]"); Assert.Single(counters); EventPipeCounterGroup mySourceGroup = counters.First(); @@ -40,7 +39,6 @@ public void GenerateCounterListTestSingleProviderWithFilter() [Fact] public void GenerateCounterListTestManyProviders() { - CounterMonitor monitor = new(); List counters = CounterMonitor.ParseProviderList("MySource1,MySource2,System.Runtime"); Assert.Equal(3, counters.Count()); Assert.Equal("MySource1", counters.ElementAt(0).ProviderName); @@ -51,7 +49,6 @@ public void GenerateCounterListTestManyProviders() [Fact] public void GenerateCounterListTestEventCountersPrefix() { - CounterMonitor monitor = new(); List counters = CounterMonitor.ParseProviderList("MySource1,EventCounters\\MySource2"); Assert.Equal(2, counters.Count()); Assert.Equal("MySource1", counters.ElementAt(0).ProviderName); @@ -63,7 +60,6 @@ public void GenerateCounterListTestEventCountersPrefix() [Fact] public void GenerateCounterListTestManyProvidersWithFilter() { - CounterMonitor monitor = new(); List counters = CounterMonitor.ParseProviderList("MySource1[mycounter1,mycounter2], MySource2[mycounter1], System.Runtime[cpu-usage,working-set]"); Assert.Equal(3, counters.Count()); @@ -83,7 +79,7 @@ public void GenerateCounterListTestManyProvidersWithFilter() [Fact] public void GenerateCounterListWithOptionAndArgumentsTest() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); List commandLineProviderArgs = new() { "System.Runtime", "MyEventSource" }; string countersOptionText = "MyEventSource1,MyEventSource2"; List counters = monitor.ConfigureCounters(countersOptionText, commandLineProviderArgs); @@ -96,7 +92,7 @@ public void GenerateCounterListWithOptionAndArgumentsTest() [Fact] public void GenerateCounterListWithOptionAndArgumentsTestWithDupEntries() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); List commandLineProviderArgs = new() { "System.Runtime", "MyEventSource" }; string countersOptionText = "System.Runtime,MyEventSource"; List counters = monitor.ConfigureCounters(countersOptionText, commandLineProviderArgs); @@ -108,7 +104,7 @@ public void GenerateCounterListWithOptionAndArgumentsTestWithDupEntries() [Fact] public void ParseErrorUnbalancedBracketsInCountersArg() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = "System.Runtime[cpu-usage,MyEventSource"; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, null)); Assert.Equal("Error parsing --counters argument: Expected to find closing ']' in counter_provider", e.Message); @@ -117,7 +113,7 @@ public void ParseErrorUnbalancedBracketsInCountersArg() [Fact] public void ParseErrorUnbalancedBracketsInCounterList() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = "System.Runtime,MyEventSource"; List commandLineProviderArgs = new() { "System.Runtime[cpu-usage", "MyEventSource" }; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, commandLineProviderArgs)); @@ -127,7 +123,7 @@ public void ParseErrorUnbalancedBracketsInCounterList() [Fact] public void ParseErrorTrailingTextInCountersArg() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = "System.Runtime[cpu-usage]hello,MyEventSource"; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, null)); Assert.Equal("Error parsing --counters argument: Unexpected characters after closing ']' in counter_provider", e.Message); @@ -136,7 +132,7 @@ public void ParseErrorTrailingTextInCountersArg() [Fact] public void ParseErrorEmptyProvider() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = ",MyEventSource"; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, null)); Assert.Equal("Error parsing --counters argument: Expected non-empty counter_provider", e.Message); @@ -145,7 +141,7 @@ public void ParseErrorEmptyProvider() [Fact] public void ParseErrorMultipleCounterLists() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = "System.Runtime[cpu-usage][working-set],MyEventSource"; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, null)); Assert.Equal("Error parsing --counters argument: Expected at most one '[' in counter_provider", e.Message); @@ -154,7 +150,7 @@ public void ParseErrorMultipleCounterLists() [Fact] public void ParseErrorMultiplePrefixesOnSameProvider() { - CounterMonitor monitor = new(); + CounterMonitor monitor = new(TextWriter.Null, TextWriter.Null); string countersOptionText = "System.Runtime,MyEventSource,EventCounters\\System.Runtime"; CommandLineErrorException e = Assert.Throws(() => monitor.ConfigureCounters(countersOptionText, null)); Assert.Equal("Error parsing --counters argument: Using the same provider name with and without the EventCounters\\ prefix in the counter list is not supported.", e.Message);