EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.Hosting", "src\SOS\SOS.Hosting\SOS.Hosting.csproj", "{ED27F39F-DF5C-4E22-87E0-EC5B5873B503}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostic.Repl", "src\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj", "{90CF2633-58F0-44EE-943B-D70207455F20}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Repl", "src\Microsoft.Diagnostics.Repl\Microsoft.Diagnostics.Repl.csproj", "{90CF2633-58F0-44EE-943B-D70207455F20}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-counters", "src\Tools\dotnet-counters\dotnet-counters.csproj", "{2A9B5988-982F-4E26-9E44-D38AC5978C30}"
EndProject
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-using System;
-
-namespace Microsoft.Diagnostics.Repl
-{
- /// <summary>
- /// Base command option attribute.
- /// </summary>
- public class BaseAttribute : Attribute
- {
- /// <summary>
- /// Name of the command
- /// </summary>
- public string Name;
-
- /// <summary>
- /// Displayed in the help for the command
- /// </summary>
- public string Help;
- }
-
- /// <summary>
- /// Marks the class as a Command.
- /// </summary>
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
- public class CommandAttribute : BaseAttribute
- {
- /// <summary>
- /// Sets the value of the CommandBase.AliasExpansion when the command is executed.
- /// </summary>
- public string AliasExpansion;
- }
-
- /// <summary>
- /// Adds an alias to the previous command attribute
- /// </summary>
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
- public class CommandAliasAttribute : BaseAttribute
- {
- }
-
- /// <summary>
- /// Marks the property as a Option.
- /// </summary>
- [AttributeUsage(AttributeTargets.Property)]
- public class OptionAttribute : BaseAttribute
- {
- }
-
- /// <summary>
- /// Adds an alias to the Option. Help is ignored.
- /// </summary>
- [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
- public class OptionAliasAttribute : BaseAttribute
- {
- }
-
- /// <summary>
- /// Marks the property the command Argument.
- /// </summary>
- [AttributeUsage(AttributeTargets.Property)]
- public class ArgumentAttribute : BaseAttribute
- {
- }
-
- /// <summary>
- /// Marks the function to invoke to execute the command.
- /// </summary>
- [AttributeUsage(AttributeTargets.Method)]
- public class CommandInvokeAttribute : Attribute
- {
- }
-
- /// <summary>
- /// Marks the function to invoke to display alternate help for command.
- /// </summary>
- [AttributeUsage(AttributeTargets.Method)]
- public class HelpInvokeAttribute : Attribute
- {
- }
-}
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-using System;
-using System.CommandLine;
-using System.CommandLine.Invocation;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Repl
-{
- /// <summary>
- /// The common command context
- /// </summary>
- public abstract class CommandBase
- {
- /// <summary>
- /// Parser invocation context. Contains the ParseResult, CommandResult, etc. Is null when
- /// InvokeAdditionalHelp is called.
- /// </summary>
- public InvocationContext InvocationContext { get; set; }
-
- /// <summary>
- /// Console instance
- /// </summary>
- public IConsole Console { get; set; }
-
- /// <summary>
- /// The AliasExpansion value from the CommandAttribute or null if none.
- /// </summary>
- public string AliasExpansion { get; set; }
-
- /// <summary>
- /// Execute the command
- /// </summary>
- [CommandInvoke]
- public abstract void Invoke();
-
- /// <summary>
- /// Display text
- /// </summary>
- /// <param name="message">text message</param>
- protected void WriteLine(string message)
- {
- Console.Out.WriteLine(message);
- }
-
- /// <summary>
- /// Display formatted text
- /// </summary>
- /// <param name="format">format string</param>
- /// <param name="args">arguments</param>
- protected void WriteLine(string format, params object[] args)
- {
- Console.Out.WriteLine(string.Format(format, args));
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.CommandLine;
-using System.CommandLine.Binding;
-using System.CommandLine.Builder;
-using System.CommandLine.Invocation;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Repl
-{
- public class CommandProcessor
- {
- private readonly Parser _parser;
- private readonly Command _rootCommand;
- private readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
- private readonly Dictionary<string, Handler> _commandHandlers = new Dictionary<string, Handler>();
-
- /// <summary>
- /// Create an instance of the command processor;
- /// </summary>
- /// <param name="console">console instance to use for commands</param>
- /// <param name="assemblies">Optional list of assemblies to look for commands</param>
- /// <param name="types">Optional list of types to look for commands</param>
- public CommandProcessor(IConsole console, IEnumerable<Assembly> assemblies = null, IEnumerable<Type> types = null)
- {
- Debug.Assert(console != null);
- Debug.Assert(assemblies != null);
- _services.Add(typeof(CommandProcessor), this);
- _services.Add(typeof(IConsole), console);
- _services.Add(typeof(IHelpBuilder), new LocalHelpBuilder(this));
- var rootBuilder = new CommandLineBuilder(new Command(">"));
- rootBuilder.UseHelp()
- .UseHelpBuilder((bindingContext) => GetService<IHelpBuilder>())
- .UseParseDirective()
- .UseSuggestDirective()
- .UseParseErrorReporting()
- .UseExceptionHandler();
- if (assemblies != null) {
- BuildCommands(rootBuilder, assemblies);
- }
- if (types != null) {
- BuildCommands(rootBuilder, types);
- }
- _rootCommand = rootBuilder.Command;
- _parser = rootBuilder.Build();
- }
-
- /// <summary>
- /// Adds a service or context to inject into an command.
- /// </summary>
- /// <typeparam name="T">type of service</typeparam>
- /// <param name="instance">service instance</param>
- public void AddService<T>(T instance)
- {
- AddService(typeof(T), instance);
- }
-
- /// <summary>
- /// Adds a service or context to inject into an command.
- /// </summary>
- /// <param name="type">service type</param>
- /// <param name="instance">service instance</param>
- public void AddService(Type type, object instance)
- {
- _services.Add(type, instance);
- }
-
- /// <summary>
- /// Parse the command line.
- /// </summary>
- /// <param name="commandLine">command line txt</param>
- /// <returns>exit code</returns>
- public Task<int> Parse(string commandLine)
- {
- ParseResult result = _parser.Parse(commandLine);
- return _parser.InvokeAsync(result, GetService<IConsole>());
- }
-
- /// <summary>
- /// Display all the help or a specific command's help.
- /// </summary>
- /// <param name="name">command name or null</param>
- /// <returns>command instance or null if not found</returns>
- public Command GetCommand(string name)
- {
- if (string.IsNullOrEmpty(name)) {
- return _rootCommand;
- }
- else {
- return _rootCommand.Children.OfType<Command>().FirstOrDefault((cmd) => name == cmd.Name || cmd.Aliases.Any((alias) => name == alias));
- }
- }
-
- private void BuildCommands(CommandLineBuilder rootBuilder, IEnumerable<Assembly> assemblies)
- {
- BuildCommands(rootBuilder, assemblies.SelectMany((assembly) => assembly.GetExportedTypes()));
- }
-
- private void BuildCommands(CommandLineBuilder rootBuilder, IEnumerable<Type> types)
- {
- foreach (Type type in types)
- {
- for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
- {
- if (baseType == typeof(CommandBase)) {
- break;
- }
- BuildCommands(rootBuilder, baseType);
- }
- }
- }
-
- private void BuildCommands(CommandLineBuilder rootBuilder, Type type)
- {
- Command command = null;
-
- var baseAttributes = (BaseAttribute[])type.GetCustomAttributes(typeof(BaseAttribute), inherit: false);
- foreach (BaseAttribute baseAttribute in baseAttributes)
- {
- if (baseAttribute is CommandAttribute commandAttribute)
- {
- command = new Command(commandAttribute.Name, commandAttribute.Help);
- var properties = new List<(PropertyInfo, Option)>();
- PropertyInfo argument = null;
-
- foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite))
- {
- var argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault();
- if (argumentAttribute != null)
- {
- if (argument != null) {
- throw new ArgumentException($"More than one ArgumentAttribute in command class: {type.Name}");
- }
- IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne;
-
- command.Argument = new Argument {
- Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(),
- Description = argumentAttribute.Help,
- ArgumentType = property.PropertyType,
- Arity = arity
- };
- argument = property;
- }
- else
- {
- var optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault();
- if (optionAttribute != null)
- {
- var option = new Option(optionAttribute.Name ?? BuildAlias(property.Name), optionAttribute.Help, new Argument { ArgumentType = property.PropertyType });
- command.AddOption(option);
- properties.Add((property, option));
-
- foreach (var optionAliasAttribute in (OptionAliasAttribute[])property.GetCustomAttributes(typeof(OptionAliasAttribute), inherit: false))
- {
- option.AddAlias(optionAliasAttribute.Name);
- }
- }
- else
- {
- // If not an option, add as just a settable properties
- properties.Add((property, null));
- }
- }
- }
-
- var handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type);
- _commandHandlers.Add(command.Name, handler);
- command.Handler = handler;
-
- rootBuilder.AddCommand(command);
- }
-
- if (baseAttribute is CommandAliasAttribute commandAliasAttribute)
- {
- if (command == null) {
- throw new ArgumentException($"No previous CommandAttribute for this CommandAliasAttribute: {type.Name}");
- }
- command.AddAlias(commandAliasAttribute.Name);
- }
- }
- }
-
- private T GetService<T>()
- {
- _services.TryGetValue(typeof(T), out object service);
- Debug.Assert(service != null);
- return (T)service;
- }
-
- private static string BuildAlias(string parameterName)
- {
- if (string.IsNullOrWhiteSpace(parameterName)) {
- throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName));
- }
- return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}";
- }
-
- class Handler : ICommandHandler
- {
- private readonly CommandProcessor _commandProcessor;
- private readonly string _aliasExpansion;
- private readonly PropertyInfo _argument;
- private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties;
-
- private readonly ConstructorInfo _constructor;
- private readonly MethodInfo _methodInfo;
- private readonly MethodInfo _methodInfoHelp;
-
- public Handler(CommandProcessor commandProcessor, string aliasExpansion, PropertyInfo argument, IEnumerable<(PropertyInfo, Option)> properties, Type type)
- {
- _commandProcessor = commandProcessor;
- _aliasExpansion = aliasExpansion;
- _argument = argument;
- _properties = properties;
-
- _constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ??
- throw new ArgumentException($"No eligible constructor found in {type}");
-
- _methodInfo = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute<CommandInvokeAttribute>() != null).SingleOrDefault() ??
- throw new ArgumentException($"No command invoke method found in {type}");
-
- _methodInfoHelp = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute<HelpInvokeAttribute>() != null).SingleOrDefault();
- }
-
- Task<int> ICommandHandler.InvokeAsync(InvocationContext context)
- {
- try
- {
- Invoke(_methodInfo, context);
- }
- catch (Exception ex)
- {
- return Task.FromException<int>(ex);
- }
- return Task.FromResult(context.ResultCode);
- }
-
- /// <summary>
- /// Executes the command's help invoke function if exists
- /// </summary>
- /// <returns>true help called, false no help function</returns>
- internal bool InvokeHelp()
- {
- if (_methodInfoHelp == null)
- {
- return false;
- }
- // The InvocationContext is null so the options and arguments in the
- // command instance created don't get set. The context for the command
- // requesting help (either the help command or some other command using
- // --help) won't work for the command instance that implements it's own
- // help (SOS command).
- Invoke(_methodInfoHelp, context: null);
- return true;
- }
-
- private void Invoke(MethodInfo methodInfo, InvocationContext context)
- {
- try
- {
- // Assumes zero parameter constructor
- object instance = _constructor.Invoke(new object[0]);
- SetProperties(context, instance);
-
- object[] arguments = BuildArguments(methodInfo, context);
- methodInfo.Invoke(instance, arguments);
- }
- catch (TargetInvocationException ex)
- {
- throw ex.InnerException;
- }
- }
-
- private void SetProperties(InvocationContext context, object instance)
- {
- IEnumerable<OptionResult> optionResults = context?.ParseResult.CommandResult.Children.OfType<OptionResult>();
-
- foreach ((PropertyInfo Property, Option Option) property in _properties)
- {
- object value = property.Property.GetValue(instance);
-
- if (property.Property.Name == nameof(CommandBase.AliasExpansion)) {
- value = _aliasExpansion;
- }
- else
- {
- Type propertyType = property.Property.PropertyType;
- if (TryGetService(propertyType, context, out object service)) {
- value = service;
- }
- else if (context != null && property.Option != null)
- {
- OptionResult optionResult = optionResults.Where((result) => result.Option == property.Option).SingleOrDefault();
- if (optionResult != null) {
- value = optionResult.GetValueOrDefault();
- }
- }
- }
-
- property.Property.SetValue(instance, value);
- }
-
- if (context != null && _argument != null)
- {
- object value = null;
- ArgumentResult result = context.ParseResult.CommandResult.ArgumentResult;
- switch (result)
- {
- case SuccessfulArgumentResult successful:
- value = successful.Value;
- break;
- case FailedArgumentResult failed:
- throw new InvalidOperationException(failed.ErrorMessage);
- }
- _argument.SetValue(instance, value);
- }
- }
-
- private object[] BuildArguments(MethodBase methodBase, InvocationContext context)
- {
- ParameterInfo[] parameters = methodBase.GetParameters();
- object[] arguments = new object[parameters.Length];
- for (int i = 0; i < parameters.Length; i++) {
- Type parameterType = parameters[i].ParameterType;
- // Ignoring false: the parameter will passed as null to allow for "optional"
- // services. The invoked method needs to check for possible null parameters.
- TryGetService(parameterType, context, out arguments[i]);
- }
- return arguments;
- }
-
- private bool TryGetService(Type type, InvocationContext context, out object service)
- {
- if (type == typeof(InvocationContext)) {
- service = context;
- }
- else if (!_commandProcessor._services.TryGetValue(type, out service)) {
- service = null;
- return false;
- }
- return true;
- }
- }
-
- class LocalHelpBuilder : IHelpBuilder
- {
- private readonly CommandProcessor _commandProcessor;
-
- public LocalHelpBuilder(CommandProcessor commandProcessor)
- {
- _commandProcessor = commandProcessor;
- }
-
- void IHelpBuilder.Write(ICommand command)
- {
- if (_commandProcessor._commandHandlers.TryGetValue(command.Name, out Handler handler))
- {
- if (handler.InvokeHelp()) {
- return;
- }
- }
- var helpBuilder = new HelpBuilder(_commandProcessor.GetService<IConsole>(), maxWidth: Console.WindowWidth);
- helpBuilder.Write(command);
- }
- }
- }
-}
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-
-using System;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Repl
-{
- public sealed class CharToLineConverter
- {
- readonly Action<string> m_callback;
- readonly StringBuilder m_text = new StringBuilder();
-
- public CharToLineConverter(Action<string> callback)
- {
- m_callback = callback;
- }
-
- public void Input(byte[] buffer, int offset, int count)
- {
- for (int i = 0; i < count; i++) {
- char c = (char)buffer[offset + i];
- if (c == '\r') {
- continue;
- }
- if (c == '\n') {
- Flush();
- }
- else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) {
- m_text.Append(c);
- }
- }
- }
-
- public void Input(string text)
- {
- foreach (char c in text) {
- if (c == '\r') {
- continue;
- }
- if (c == '\n') {
- Flush();
- }
- else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) {
- m_text.Append(c);
- }
- }
- }
-
- public void Flush()
- {
- m_callback(m_text.ToString());
- m_text.Clear();
- }
- }
-}
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-using System;
-using System.Collections.Generic;
-using System.CommandLine;
-using System.IO;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Repl
-{
- public sealed class ConsoleProvider : IConsole
- {
- private readonly List<StringBuilder> m_history;
-
- private readonly CharToLineConverter m_consoleConverter;
- private readonly CharToLineConverter m_warningConverter;
- private readonly CharToLineConverter m_errorConverter;
-
- private string m_prompt = "> ";
-
- private bool m_shutdown;
- private CancellationTokenSource m_interruptExecutingCommand;
-
- private string m_clearLine;
- private bool m_interactiveConsole;
- private bool m_refreshingLine;
- private StringBuilder m_activeLine;
-
- private int m_selectedHistory;
-
- private bool m_modified;
- private int m_cursorPosition;
- private int m_scrollPosition;
- private bool m_insertMode;
-
- private int m_commandExecuting;
- private string m_lastCommandLine;
-
- /// <summary>
- /// Create an instance of the console provider
- /// </summary>
- /// <param name="errorColor">error color (default red)</param>
- /// <param name="warningColor">warning color (default yellow)</param>
- public ConsoleProvider(ConsoleColor errorColor = ConsoleColor.Red, ConsoleColor warningColor = ConsoleColor.Yellow)
- {
- m_history = new List<StringBuilder>();
- m_activeLine = new StringBuilder();
-
- m_consoleConverter = new CharToLineConverter(text => {
- NewOutput(text);
- });
-
- m_warningConverter = new CharToLineConverter(text => {
- NewOutput(text, warningColor);
- });
-
- m_errorConverter = new CharToLineConverter(text => {
- NewOutput(text, errorColor);
- });
-
- Out = new StandardStreamWriter((text) => WriteOutput(OutputType.Normal, text));
- Error = new StandardStreamWriter((text) => WriteOutput(OutputType.Error, text));
-
- // Hook ctrl-C and ctrl-break
- Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
- }
-
- /// <summary>
- /// Start input processing and command dispatching
- /// </summary>
- /// <param name="dispatchCommand">Called to dispatch a command on ENTER</param>
- public async Task Start(Func<string, CancellationToken, Task> dispatchCommand)
- {
- m_lastCommandLine = null;
- m_shutdown = false;
- m_interactiveConsole = !Console.IsInputRedirected;
- RefreshLine();
-
- // Start keyboard processing
- while (!m_shutdown) {
- if (m_interactiveConsole)
- {
- ConsoleKeyInfo keyInfo = Console.ReadKey(true);
- await ProcessKeyInfo(keyInfo, dispatchCommand);
- }
- else
- {
- // The input has been redirected (i.e. testing or in script)
- WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
- string line = Console.ReadLine();
- if (string.IsNullOrEmpty(line)) {
- continue;
- }
- await Dispatch(line, dispatchCommand);
- }
- }
- }
-
- /// <summary>
- /// Stop input processing/dispatching
- /// </summary>
- public void Stop()
- {
- ClearLine();
- m_shutdown = true;
- Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
- }
-
- /// <summary>
- /// Change the command prompt
- /// </summary>
- /// <param name="prompt">new prompt</param>
- public void SetPrompt(string prompt)
- {
- m_prompt = prompt;
- RefreshLine();
- }
-
- /// <summary>
- /// Writes a message with a new line to console.
- /// </summary>
- public void WriteLine(OutputType type, string format, params object[] parameters)
- {
- WriteOutput(type, string.Format(format + Environment.NewLine, parameters));
- }
-
- /// <summary>
- /// Write text on the console screen
- /// </summary>
- /// <param name="type">output type</param>
- /// <param name="message">text</param>
- /// <exception cref="OperationCanceledException">ctrl-c interrupted the command</exception>
- public void WriteOutput(OutputType type, string message)
- {
- switch (type)
- {
- case OutputType.Normal:
- m_consoleConverter.Input(message);
- break;
-
- case OutputType.Warning:
- m_warningConverter.Input(message);
- break;
-
- case OutputType.Error:
- m_errorConverter.Input(message);
- break;
- }
- }
-
- /// <summary>
- /// Clear the console screen
- /// </summary>
- public void ClearScreen()
- {
- Console.Clear();
- PrintActiveLine();
- }
-
- /// <summary>
- /// Write a line to the console.
- /// </summary>
- /// <param name="text">line of text</param>
- /// <param name="color">color of the text</param>
- private void NewOutput(string text, ConsoleColor? color = null)
- {
- ClearLine();
-
- ConsoleColor? originalColor = null;
- if (color.HasValue) {
- originalColor = Console.ForegroundColor;
- Console.ForegroundColor = color.Value;
- }
- Console.WriteLine(text);
- if (originalColor.HasValue) {
- Console.ForegroundColor = originalColor.Value;
- }
-
- PrintActiveLine();
- }
-
- /// <summary>
- /// This is the ctrl-c/ctrl-break handler
- /// </summary>
- private void OnCtrlBreakKeyPress(object sender, ConsoleCancelEventArgs e)
- {
- if (!m_shutdown && m_interactiveConsole) {
- if (m_interruptExecutingCommand != null) {
- m_interruptExecutingCommand.Cancel();
- }
- e.Cancel = true;
- }
- }
-
- private void CommandStarting()
- {
- if (m_commandExecuting == 0) {
- ClearLine();
- }
- m_commandExecuting++;
- }
-
- private void CommandFinished()
- {
- if (--m_commandExecuting == 0) {
- RefreshLine();
- }
- }
-
- private void ClearLine()
- {
- if (!m_interactiveConsole) {
- return;
- }
-
- if (m_commandExecuting != 0) {
- return;
- }
-
- if (m_clearLine == null || m_clearLine.Length != Console.WindowWidth) {
- m_clearLine = "\r" + new string(' ', Console.WindowWidth - 1);
- }
-
- Console.Write(m_clearLine);
- Console.CursorLeft = 0;
- }
-
- private void PrintActiveLine()
- {
- if (!m_interactiveConsole) {
- return;
- }
-
- if (m_shutdown) {
- return;
- }
-
- if (m_commandExecuting != 0) {
- return;
- }
-
- string prompt = m_prompt;
-
- int lineWidth = 80;
- if (Console.WindowWidth > prompt.Length) {
- lineWidth = Console.WindowWidth - prompt.Length - 1;
- }
- int scrollIncrement = lineWidth / 3;
-
- int activeLineLen = m_activeLine.Length;
-
- m_scrollPosition = Math.Min(Math.Max(m_scrollPosition, 0), activeLineLen);
- m_cursorPosition = Math.Min(Math.Max(m_cursorPosition, 0), activeLineLen);
-
- while (m_cursorPosition < m_scrollPosition) {
- m_scrollPosition = Math.Max(m_scrollPosition - scrollIncrement, 0);
- }
-
- while (m_cursorPosition - m_scrollPosition > lineWidth - 5) {
- m_scrollPosition += scrollIncrement;
- }
-
- int lineRest = activeLineLen - m_scrollPosition;
- int max = Math.Min(lineRest, lineWidth);
- string text = m_activeLine.ToString(m_scrollPosition, max);
-
- Console.Write("{0}{1}", prompt, text);
- Console.CursorLeft = prompt.Length + (m_cursorPosition - m_scrollPosition);
- }
-
- private void RefreshLine()
- {
- // Check for recursions.
- if (m_refreshingLine) {
- return;
- }
- m_refreshingLine = true;
- ClearLine();
- PrintActiveLine();
- m_refreshingLine = false;
- }
-
- private async Task ProcessKeyInfo(ConsoleKeyInfo keyInfo, Func<string, CancellationToken, Task> dispatchCommand)
- {
- int activeLineLen = m_activeLine.Length;
-
- switch (keyInfo.Key) {
- case ConsoleKey.Backspace: // The BACKSPACE key.
- if (m_cursorPosition > 0) {
- EnsureNewEntry();
- m_activeLine.Remove(m_cursorPosition - 1, 1);
- m_cursorPosition--;
- RefreshLine();
- }
- break;
-
- case ConsoleKey.Insert: // The INS (INSERT) key.
- m_insertMode = !m_insertMode;
- RefreshLine();
- break;
-
- case ConsoleKey.Delete: // The DEL (DELETE) key.
- if (m_cursorPosition < activeLineLen) {
- EnsureNewEntry();
- m_activeLine.Remove(m_cursorPosition, 1);
- RefreshLine();
- }
- break;
-
- case ConsoleKey.Enter: // The ENTER key.
- string newCommand = m_activeLine.ToString();
-
- if (m_modified) {
- m_history.Add(m_activeLine);
- }
- m_selectedHistory = m_history.Count;
-
- await Dispatch(newCommand, dispatchCommand);
-
- SwitchToHistoryEntry();
- break;
-
- case ConsoleKey.Escape: // The ESC (ESCAPE) key.
- EnsureNewEntry();
- m_activeLine.Clear();
- m_cursorPosition = 0;
- RefreshLine();
- break;
-
- case ConsoleKey.End: // The END key.
- m_cursorPosition = activeLineLen;
- RefreshLine();
- break;
-
- case ConsoleKey.Home: // The HOME key.
- m_cursorPosition = 0;
- RefreshLine();
- break;
-
- case ConsoleKey.LeftArrow: // The LEFT ARROW key.
- if (keyInfo.Modifiers == ConsoleModifiers.Control) {
- while (m_cursorPosition > 0 && char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) {
- m_cursorPosition--;
- }
-
- while (m_cursorPosition > 0 && !char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) {
- m_cursorPosition--;
- }
- }
- else {
- m_cursorPosition--;
- }
-
- RefreshLine();
- break;
-
- case ConsoleKey.UpArrow: // The UP ARROW key.
- if (m_selectedHistory > 0) {
- m_selectedHistory--;
- }
- SwitchToHistoryEntry();
- break;
-
- case ConsoleKey.RightArrow: // The RIGHT ARROW key.
- if (keyInfo.Modifiers == ConsoleModifiers.Control) {
- while (m_cursorPosition < activeLineLen && !char.IsWhiteSpace(m_activeLine[m_cursorPosition])) {
- m_cursorPosition++;
- }
-
- while (m_cursorPosition < activeLineLen && char.IsWhiteSpace(m_activeLine[m_cursorPosition])) {
- m_cursorPosition++;
- }
- }
- else {
- m_cursorPosition++;
- }
-
- RefreshLine();
- break;
-
- case ConsoleKey.DownArrow: // The DOWN ARROW key.
- if (m_selectedHistory < m_history.Count) {
- m_selectedHistory++;
- }
- SwitchToHistoryEntry();
-
- RefreshLine();
- break;
-
- default:
- if (keyInfo.KeyChar != 0) {
- if ((keyInfo.Modifiers & (ConsoleModifiers.Control | ConsoleModifiers.Alt)) == 0) {
- AppendNewText(new string(keyInfo.KeyChar, 1));
- }
- }
- break;
- }
- }
-
- private async Task Dispatch(string newCommand, Func<string, CancellationToken, Task> dispatchCommand)
- {
- CommandStarting();
- m_interruptExecutingCommand = new CancellationTokenSource();
- try
- {
- newCommand = newCommand.Trim();
- if (string.IsNullOrEmpty(newCommand) && m_lastCommandLine != null) {
- newCommand = m_lastCommandLine;
- }
- try
- {
- WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand);
- await dispatchCommand(newCommand, m_interruptExecutingCommand.Token);
- m_lastCommandLine = newCommand;
- }
- catch (OperationCanceledException)
- {
- // ctrl-c interrupted the command
- m_lastCommandLine = null;
- }
- catch (Exception ex) when (!(ex is NullReferenceException || ex is ArgumentNullException || ex is ArgumentException))
- {
- WriteLine(OutputType.Error, "ERROR: {0}", ex.Message);
- m_lastCommandLine = null;
- }
- }
- finally
- {
- m_interruptExecutingCommand = null;
- CommandFinished();
- }
- }
-
- private void AppendNewText(string text)
- {
- EnsureNewEntry();
-
- foreach (char c in text) {
- // Filter unwanted characters.
- switch (c) {
- case '\t':
- case '\r':
- case '\n':
- continue;
- }
-
- if (m_insertMode && m_cursorPosition < m_activeLine.Length) {
- m_activeLine[m_cursorPosition] = c;
- }
- else {
- m_activeLine.Insert(m_cursorPosition, c);
- }
- m_modified = true;
- m_cursorPosition++;
- }
-
- RefreshLine();
- }
-
- private void SwitchToHistoryEntry()
- {
- if (m_selectedHistory < m_history.Count) {
- m_activeLine = m_history[m_selectedHistory];
- }
- else {
- m_activeLine = new StringBuilder();
- }
-
- m_cursorPosition = m_activeLine.Length;
- m_modified = false;
-
- RefreshLine();
- }
-
- private void EnsureNewEntry()
- {
- if (!m_modified) {
- m_activeLine = new StringBuilder(m_activeLine.ToString());
- m_modified = true;
- }
- }
-
- #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; } }
-
- class StandardStreamWriter : IStandardStreamWriter
- {
- readonly Action<string> _write;
-
- public StandardStreamWriter(Action<string> write) => _write = write;
-
- void IStandardStreamWriter.Write(string value) => _write(value);
- }
-
- #endregion
- }
-}
+++ /dev/null
-// --------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-// --------------------------------------------------------------------
-
-namespace Microsoft.Diagnostics.Repl
-{
- /// <summary>
- /// The type of output.
- /// </summary>
- public enum OutputType
- {
- Normal = 1,
- Error = 2,
- Warning = 3,
- }
-}
+++ /dev/null
-<!-- Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
-<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>netstandard2.0</TargetFramework>
- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
- <NoWarn>;1591;1701</NoWarn>
- <Description>Diagnostic utility functions and helpers</Description>
- <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
-
- <ItemGroup>
- <PackageReference Include="System.CommandLine.Experimental" Version="$(SystemCommandLineExperimentalVersion)" />
- </ItemGroup>
-
-</Project>
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+using System;
+
+namespace Microsoft.Diagnostics.Repl
+{
+ /// <summary>
+ /// Base command option attribute.
+ /// </summary>
+ public class BaseAttribute : Attribute
+ {
+ /// <summary>
+ /// Name of the command
+ /// </summary>
+ public string Name;
+
+ /// <summary>
+ /// Displayed in the help for the command
+ /// </summary>
+ public string Help;
+ }
+
+ /// <summary>
+ /// Marks the class as a Command.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class CommandAttribute : BaseAttribute
+ {
+ /// <summary>
+ /// Sets the value of the CommandBase.AliasExpansion when the command is executed.
+ /// </summary>
+ public string AliasExpansion;
+ }
+
+ /// <summary>
+ /// Adds an alias to the previous command attribute
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class CommandAliasAttribute : BaseAttribute
+ {
+ }
+
+ /// <summary>
+ /// Marks the property as a Option.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property)]
+ public class OptionAttribute : BaseAttribute
+ {
+ }
+
+ /// <summary>
+ /// Adds an alias to the Option. Help is ignored.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+ public class OptionAliasAttribute : BaseAttribute
+ {
+ }
+
+ /// <summary>
+ /// Marks the property the command Argument.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ArgumentAttribute : BaseAttribute
+ {
+ }
+
+ /// <summary>
+ /// Marks the function to invoke to execute the command.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public class CommandInvokeAttribute : Attribute
+ {
+ }
+
+ /// <summary>
+ /// Marks the function to invoke to display alternate help for command.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public class HelpInvokeAttribute : Attribute
+ {
+ }
+}
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+using System;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.Repl
+{
+ /// <summary>
+ /// The common command context
+ /// </summary>
+ public abstract class CommandBase
+ {
+ /// <summary>
+ /// Parser invocation context. Contains the ParseResult, CommandResult, etc. Is null when
+ /// InvokeAdditionalHelp is called.
+ /// </summary>
+ public InvocationContext InvocationContext { get; set; }
+
+ /// <summary>
+ /// Console instance
+ /// </summary>
+ public IConsole Console { get; set; }
+
+ /// <summary>
+ /// The AliasExpansion value from the CommandAttribute or null if none.
+ /// </summary>
+ public string AliasExpansion { get; set; }
+
+ /// <summary>
+ /// Execute the command
+ /// </summary>
+ [CommandInvoke]
+ public abstract void Invoke();
+
+ /// <summary>
+ /// Display text
+ /// </summary>
+ /// <param name="message">text message</param>
+ protected void WriteLine(string message)
+ {
+ Console.Out.WriteLine(message);
+ }
+
+ /// <summary>
+ /// Display formatted text
+ /// </summary>
+ /// <param name="format">format string</param>
+ /// <param name="args">arguments</param>
+ protected void WriteLine(string format, params object[] args)
+ {
+ Console.Out.WriteLine(string.Format(format, args));
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Binding;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.Repl
+{
+ public class CommandProcessor
+ {
+ private readonly Parser _parser;
+ private readonly Command _rootCommand;
+ private readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
+ private readonly Dictionary<string, Handler> _commandHandlers = new Dictionary<string, Handler>();
+
+ /// <summary>
+ /// Create an instance of the command processor;
+ /// </summary>
+ /// <param name="console">console instance to use for commands</param>
+ /// <param name="assemblies">Optional list of assemblies to look for commands</param>
+ /// <param name="types">Optional list of types to look for commands</param>
+ public CommandProcessor(IConsole console, IEnumerable<Assembly> assemblies = null, IEnumerable<Type> types = null)
+ {
+ Debug.Assert(console != null);
+ Debug.Assert(assemblies != null);
+ _services.Add(typeof(CommandProcessor), this);
+ _services.Add(typeof(IConsole), console);
+ _services.Add(typeof(IHelpBuilder), new LocalHelpBuilder(this));
+ var rootBuilder = new CommandLineBuilder(new Command(">"));
+ rootBuilder.UseHelp()
+ .UseHelpBuilder((bindingContext) => GetService<IHelpBuilder>())
+ .UseParseDirective()
+ .UseSuggestDirective()
+ .UseParseErrorReporting()
+ .UseExceptionHandler();
+ if (assemblies != null) {
+ BuildCommands(rootBuilder, assemblies);
+ }
+ if (types != null) {
+ BuildCommands(rootBuilder, types);
+ }
+ _rootCommand = rootBuilder.Command;
+ _parser = rootBuilder.Build();
+ }
+
+ /// <summary>
+ /// Adds a service or context to inject into an command.
+ /// </summary>
+ /// <typeparam name="T">type of service</typeparam>
+ /// <param name="instance">service instance</param>
+ public void AddService<T>(T instance)
+ {
+ AddService(typeof(T), instance);
+ }
+
+ /// <summary>
+ /// Adds a service or context to inject into an command.
+ /// </summary>
+ /// <param name="type">service type</param>
+ /// <param name="instance">service instance</param>
+ public void AddService(Type type, object instance)
+ {
+ _services.Add(type, instance);
+ }
+
+ /// <summary>
+ /// Parse the command line.
+ /// </summary>
+ /// <param name="commandLine">command line txt</param>
+ /// <returns>exit code</returns>
+ public Task<int> Parse(string commandLine)
+ {
+ ParseResult result = _parser.Parse(commandLine);
+ return _parser.InvokeAsync(result, GetService<IConsole>());
+ }
+
+ /// <summary>
+ /// Display all the help or a specific command's help.
+ /// </summary>
+ /// <param name="name">command name or null</param>
+ /// <returns>command instance or null if not found</returns>
+ public Command GetCommand(string name)
+ {
+ if (string.IsNullOrEmpty(name)) {
+ return _rootCommand;
+ }
+ else {
+ return _rootCommand.Children.OfType<Command>().FirstOrDefault((cmd) => name == cmd.Name || cmd.Aliases.Any((alias) => name == alias));
+ }
+ }
+
+ private void BuildCommands(CommandLineBuilder rootBuilder, IEnumerable<Assembly> assemblies)
+ {
+ BuildCommands(rootBuilder, assemblies.SelectMany((assembly) => assembly.GetExportedTypes()));
+ }
+
+ private void BuildCommands(CommandLineBuilder rootBuilder, IEnumerable<Type> types)
+ {
+ foreach (Type type in types)
+ {
+ for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
+ {
+ if (baseType == typeof(CommandBase)) {
+ break;
+ }
+ BuildCommands(rootBuilder, baseType);
+ }
+ }
+ }
+
+ private void BuildCommands(CommandLineBuilder rootBuilder, Type type)
+ {
+ Command command = null;
+
+ var baseAttributes = (BaseAttribute[])type.GetCustomAttributes(typeof(BaseAttribute), inherit: false);
+ foreach (BaseAttribute baseAttribute in baseAttributes)
+ {
+ if (baseAttribute is CommandAttribute commandAttribute)
+ {
+ command = new Command(commandAttribute.Name, commandAttribute.Help);
+ var properties = new List<(PropertyInfo, Option)>();
+ PropertyInfo argument = null;
+
+ foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite))
+ {
+ var argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault();
+ if (argumentAttribute != null)
+ {
+ if (argument != null) {
+ throw new ArgumentException($"More than one ArgumentAttribute in command class: {type.Name}");
+ }
+ IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne;
+
+ command.Argument = new Argument {
+ Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(),
+ Description = argumentAttribute.Help,
+ ArgumentType = property.PropertyType,
+ Arity = arity
+ };
+ argument = property;
+ }
+ else
+ {
+ var optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault();
+ if (optionAttribute != null)
+ {
+ var option = new Option(optionAttribute.Name ?? BuildAlias(property.Name), optionAttribute.Help, new Argument { ArgumentType = property.PropertyType });
+ command.AddOption(option);
+ properties.Add((property, option));
+
+ foreach (var optionAliasAttribute in (OptionAliasAttribute[])property.GetCustomAttributes(typeof(OptionAliasAttribute), inherit: false))
+ {
+ option.AddAlias(optionAliasAttribute.Name);
+ }
+ }
+ else
+ {
+ // If not an option, add as just a settable properties
+ properties.Add((property, null));
+ }
+ }
+ }
+
+ var handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type);
+ _commandHandlers.Add(command.Name, handler);
+ command.Handler = handler;
+
+ rootBuilder.AddCommand(command);
+ }
+
+ if (baseAttribute is CommandAliasAttribute commandAliasAttribute)
+ {
+ if (command == null) {
+ throw new ArgumentException($"No previous CommandAttribute for this CommandAliasAttribute: {type.Name}");
+ }
+ command.AddAlias(commandAliasAttribute.Name);
+ }
+ }
+ }
+
+ private T GetService<T>()
+ {
+ _services.TryGetValue(typeof(T), out object service);
+ Debug.Assert(service != null);
+ return (T)service;
+ }
+
+ private static string BuildAlias(string parameterName)
+ {
+ if (string.IsNullOrWhiteSpace(parameterName)) {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName));
+ }
+ return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}";
+ }
+
+ class Handler : ICommandHandler
+ {
+ private readonly CommandProcessor _commandProcessor;
+ private readonly string _aliasExpansion;
+ private readonly PropertyInfo _argument;
+ private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties;
+
+ private readonly ConstructorInfo _constructor;
+ private readonly MethodInfo _methodInfo;
+ private readonly MethodInfo _methodInfoHelp;
+
+ public Handler(CommandProcessor commandProcessor, string aliasExpansion, PropertyInfo argument, IEnumerable<(PropertyInfo, Option)> properties, Type type)
+ {
+ _commandProcessor = commandProcessor;
+ _aliasExpansion = aliasExpansion;
+ _argument = argument;
+ _properties = properties;
+
+ _constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ??
+ throw new ArgumentException($"No eligible constructor found in {type}");
+
+ _methodInfo = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute<CommandInvokeAttribute>() != null).SingleOrDefault() ??
+ throw new ArgumentException($"No command invoke method found in {type}");
+
+ _methodInfoHelp = type.GetMethods().Where((methodInfo) => methodInfo.GetCustomAttribute<HelpInvokeAttribute>() != null).SingleOrDefault();
+ }
+
+ Task<int> ICommandHandler.InvokeAsync(InvocationContext context)
+ {
+ try
+ {
+ Invoke(_methodInfo, context);
+ }
+ catch (Exception ex)
+ {
+ return Task.FromException<int>(ex);
+ }
+ return Task.FromResult(context.ResultCode);
+ }
+
+ /// <summary>
+ /// Executes the command's help invoke function if exists
+ /// </summary>
+ /// <returns>true help called, false no help function</returns>
+ internal bool InvokeHelp()
+ {
+ if (_methodInfoHelp == null)
+ {
+ return false;
+ }
+ // The InvocationContext is null so the options and arguments in the
+ // command instance created don't get set. The context for the command
+ // requesting help (either the help command or some other command using
+ // --help) won't work for the command instance that implements it's own
+ // help (SOS command).
+ Invoke(_methodInfoHelp, context: null);
+ return true;
+ }
+
+ private void Invoke(MethodInfo methodInfo, InvocationContext context)
+ {
+ try
+ {
+ // Assumes zero parameter constructor
+ object instance = _constructor.Invoke(new object[0]);
+ SetProperties(context, instance);
+
+ object[] arguments = BuildArguments(methodInfo, context);
+ methodInfo.Invoke(instance, arguments);
+ }
+ catch (TargetInvocationException ex)
+ {
+ throw ex.InnerException;
+ }
+ }
+
+ private void SetProperties(InvocationContext context, object instance)
+ {
+ IEnumerable<OptionResult> optionResults = context?.ParseResult.CommandResult.Children.OfType<OptionResult>();
+
+ foreach ((PropertyInfo Property, Option Option) property in _properties)
+ {
+ object value = property.Property.GetValue(instance);
+
+ if (property.Property.Name == nameof(CommandBase.AliasExpansion)) {
+ value = _aliasExpansion;
+ }
+ else
+ {
+ Type propertyType = property.Property.PropertyType;
+ if (TryGetService(propertyType, context, out object service)) {
+ value = service;
+ }
+ else if (context != null && property.Option != null)
+ {
+ OptionResult optionResult = optionResults.Where((result) => result.Option == property.Option).SingleOrDefault();
+ if (optionResult != null) {
+ value = optionResult.GetValueOrDefault();
+ }
+ }
+ }
+
+ property.Property.SetValue(instance, value);
+ }
+
+ if (context != null && _argument != null)
+ {
+ object value = null;
+ ArgumentResult result = context.ParseResult.CommandResult.ArgumentResult;
+ switch (result)
+ {
+ case SuccessfulArgumentResult successful:
+ value = successful.Value;
+ break;
+ case FailedArgumentResult failed:
+ throw new InvalidOperationException(failed.ErrorMessage);
+ }
+ _argument.SetValue(instance, value);
+ }
+ }
+
+ private object[] BuildArguments(MethodBase methodBase, InvocationContext context)
+ {
+ ParameterInfo[] parameters = methodBase.GetParameters();
+ object[] arguments = new object[parameters.Length];
+ for (int i = 0; i < parameters.Length; i++) {
+ Type parameterType = parameters[i].ParameterType;
+ // Ignoring false: the parameter will passed as null to allow for "optional"
+ // services. The invoked method needs to check for possible null parameters.
+ TryGetService(parameterType, context, out arguments[i]);
+ }
+ return arguments;
+ }
+
+ private bool TryGetService(Type type, InvocationContext context, out object service)
+ {
+ if (type == typeof(InvocationContext)) {
+ service = context;
+ }
+ else if (!_commandProcessor._services.TryGetValue(type, out service)) {
+ service = null;
+ return false;
+ }
+ return true;
+ }
+ }
+
+ class LocalHelpBuilder : IHelpBuilder
+ {
+ private readonly CommandProcessor _commandProcessor;
+
+ public LocalHelpBuilder(CommandProcessor commandProcessor)
+ {
+ _commandProcessor = commandProcessor;
+ }
+
+ void IHelpBuilder.Write(ICommand command)
+ {
+ if (_commandProcessor._commandHandlers.TryGetValue(command.Name, out Handler handler))
+ {
+ if (handler.InvokeHelp()) {
+ return;
+ }
+ }
+ var helpBuilder = new HelpBuilder(_commandProcessor.GetService<IConsole>(), maxWidth: Console.WindowWidth);
+ helpBuilder.Write(command);
+ }
+ }
+ }
+}
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+
+using System;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Repl
+{
+ public sealed class CharToLineConverter
+ {
+ readonly Action<string> m_callback;
+ readonly StringBuilder m_text = new StringBuilder();
+
+ public CharToLineConverter(Action<string> callback)
+ {
+ m_callback = callback;
+ }
+
+ public void Input(byte[] buffer, int offset, int count)
+ {
+ for (int i = 0; i < count; i++) {
+ char c = (char)buffer[offset + i];
+ if (c == '\r') {
+ continue;
+ }
+ if (c == '\n') {
+ Flush();
+ }
+ else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) {
+ m_text.Append(c);
+ }
+ }
+ }
+
+ public void Input(string text)
+ {
+ foreach (char c in text) {
+ if (c == '\r') {
+ continue;
+ }
+ if (c == '\n') {
+ Flush();
+ }
+ else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) {
+ m_text.Append(c);
+ }
+ }
+ }
+
+ public void Flush()
+ {
+ m_callback(m_text.ToString());
+ m_text.Clear();
+ }
+ }
+}
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.Repl
+{
+ public sealed class ConsoleProvider : IConsole
+ {
+ private readonly List<StringBuilder> m_history;
+
+ private readonly CharToLineConverter m_consoleConverter;
+ private readonly CharToLineConverter m_warningConverter;
+ private readonly CharToLineConverter m_errorConverter;
+
+ private string m_prompt = "> ";
+
+ private bool m_shutdown;
+ private CancellationTokenSource m_interruptExecutingCommand;
+
+ private string m_clearLine;
+ private bool m_interactiveConsole;
+ private bool m_refreshingLine;
+ private StringBuilder m_activeLine;
+
+ private int m_selectedHistory;
+
+ private bool m_modified;
+ private int m_cursorPosition;
+ private int m_scrollPosition;
+ private bool m_insertMode;
+
+ private int m_commandExecuting;
+ private string m_lastCommandLine;
+
+ /// <summary>
+ /// Create an instance of the console provider
+ /// </summary>
+ /// <param name="errorColor">error color (default red)</param>
+ /// <param name="warningColor">warning color (default yellow)</param>
+ public ConsoleProvider(ConsoleColor errorColor = ConsoleColor.Red, ConsoleColor warningColor = ConsoleColor.Yellow)
+ {
+ m_history = new List<StringBuilder>();
+ m_activeLine = new StringBuilder();
+
+ m_consoleConverter = new CharToLineConverter(text => {
+ NewOutput(text);
+ });
+
+ m_warningConverter = new CharToLineConverter(text => {
+ NewOutput(text, warningColor);
+ });
+
+ m_errorConverter = new CharToLineConverter(text => {
+ NewOutput(text, errorColor);
+ });
+
+ Out = new StandardStreamWriter((text) => WriteOutput(OutputType.Normal, text));
+ Error = new StandardStreamWriter((text) => WriteOutput(OutputType.Error, text));
+
+ // Hook ctrl-C and ctrl-break
+ Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
+ }
+
+ /// <summary>
+ /// Start input processing and command dispatching
+ /// </summary>
+ /// <param name="dispatchCommand">Called to dispatch a command on ENTER</param>
+ public async Task Start(Func<string, CancellationToken, Task> dispatchCommand)
+ {
+ m_lastCommandLine = null;
+ m_shutdown = false;
+ m_interactiveConsole = !Console.IsInputRedirected;
+ RefreshLine();
+
+ // Start keyboard processing
+ while (!m_shutdown) {
+ if (m_interactiveConsole)
+ {
+ ConsoleKeyInfo keyInfo = Console.ReadKey(true);
+ await ProcessKeyInfo(keyInfo, dispatchCommand);
+ }
+ else
+ {
+ // The input has been redirected (i.e. testing or in script)
+ WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
+ string line = Console.ReadLine();
+ if (string.IsNullOrEmpty(line)) {
+ continue;
+ }
+ await Dispatch(line, dispatchCommand);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stop input processing/dispatching
+ /// </summary>
+ public void Stop()
+ {
+ ClearLine();
+ m_shutdown = true;
+ Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
+ }
+
+ /// <summary>
+ /// Change the command prompt
+ /// </summary>
+ /// <param name="prompt">new prompt</param>
+ public void SetPrompt(string prompt)
+ {
+ m_prompt = prompt;
+ RefreshLine();
+ }
+
+ /// <summary>
+ /// Writes a message with a new line to console.
+ /// </summary>
+ public void WriteLine(OutputType type, string format, params object[] parameters)
+ {
+ WriteOutput(type, string.Format(format + Environment.NewLine, parameters));
+ }
+
+ /// <summary>
+ /// Write text on the console screen
+ /// </summary>
+ /// <param name="type">output type</param>
+ /// <param name="message">text</param>
+ /// <exception cref="OperationCanceledException">ctrl-c interrupted the command</exception>
+ public void WriteOutput(OutputType type, string message)
+ {
+ switch (type)
+ {
+ case OutputType.Normal:
+ m_consoleConverter.Input(message);
+ break;
+
+ case OutputType.Warning:
+ m_warningConverter.Input(message);
+ break;
+
+ case OutputType.Error:
+ m_errorConverter.Input(message);
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Clear the console screen
+ /// </summary>
+ public void ClearScreen()
+ {
+ Console.Clear();
+ PrintActiveLine();
+ }
+
+ /// <summary>
+ /// Write a line to the console.
+ /// </summary>
+ /// <param name="text">line of text</param>
+ /// <param name="color">color of the text</param>
+ private void NewOutput(string text, ConsoleColor? color = null)
+ {
+ ClearLine();
+
+ ConsoleColor? originalColor = null;
+ if (color.HasValue) {
+ originalColor = Console.ForegroundColor;
+ Console.ForegroundColor = color.Value;
+ }
+ Console.WriteLine(text);
+ if (originalColor.HasValue) {
+ Console.ForegroundColor = originalColor.Value;
+ }
+
+ PrintActiveLine();
+ }
+
+ /// <summary>
+ /// This is the ctrl-c/ctrl-break handler
+ /// </summary>
+ private void OnCtrlBreakKeyPress(object sender, ConsoleCancelEventArgs e)
+ {
+ if (!m_shutdown && m_interactiveConsole) {
+ if (m_interruptExecutingCommand != null) {
+ m_interruptExecutingCommand.Cancel();
+ }
+ e.Cancel = true;
+ }
+ }
+
+ private void CommandStarting()
+ {
+ if (m_commandExecuting == 0) {
+ ClearLine();
+ }
+ m_commandExecuting++;
+ }
+
+ private void CommandFinished()
+ {
+ if (--m_commandExecuting == 0) {
+ RefreshLine();
+ }
+ }
+
+ private void ClearLine()
+ {
+ if (!m_interactiveConsole) {
+ return;
+ }
+
+ if (m_commandExecuting != 0) {
+ return;
+ }
+
+ if (m_clearLine == null || m_clearLine.Length != Console.WindowWidth) {
+ m_clearLine = "\r" + new string(' ', Console.WindowWidth - 1);
+ }
+
+ Console.Write(m_clearLine);
+ Console.CursorLeft = 0;
+ }
+
+ private void PrintActiveLine()
+ {
+ if (!m_interactiveConsole) {
+ return;
+ }
+
+ if (m_shutdown) {
+ return;
+ }
+
+ if (m_commandExecuting != 0) {
+ return;
+ }
+
+ string prompt = m_prompt;
+
+ int lineWidth = 80;
+ if (Console.WindowWidth > prompt.Length) {
+ lineWidth = Console.WindowWidth - prompt.Length - 1;
+ }
+ int scrollIncrement = lineWidth / 3;
+
+ int activeLineLen = m_activeLine.Length;
+
+ m_scrollPosition = Math.Min(Math.Max(m_scrollPosition, 0), activeLineLen);
+ m_cursorPosition = Math.Min(Math.Max(m_cursorPosition, 0), activeLineLen);
+
+ while (m_cursorPosition < m_scrollPosition) {
+ m_scrollPosition = Math.Max(m_scrollPosition - scrollIncrement, 0);
+ }
+
+ while (m_cursorPosition - m_scrollPosition > lineWidth - 5) {
+ m_scrollPosition += scrollIncrement;
+ }
+
+ int lineRest = activeLineLen - m_scrollPosition;
+ int max = Math.Min(lineRest, lineWidth);
+ string text = m_activeLine.ToString(m_scrollPosition, max);
+
+ Console.Write("{0}{1}", prompt, text);
+ Console.CursorLeft = prompt.Length + (m_cursorPosition - m_scrollPosition);
+ }
+
+ private void RefreshLine()
+ {
+ // Check for recursions.
+ if (m_refreshingLine) {
+ return;
+ }
+ m_refreshingLine = true;
+ ClearLine();
+ PrintActiveLine();
+ m_refreshingLine = false;
+ }
+
+ private async Task ProcessKeyInfo(ConsoleKeyInfo keyInfo, Func<string, CancellationToken, Task> dispatchCommand)
+ {
+ int activeLineLen = m_activeLine.Length;
+
+ switch (keyInfo.Key) {
+ case ConsoleKey.Backspace: // The BACKSPACE key.
+ if (m_cursorPosition > 0) {
+ EnsureNewEntry();
+ m_activeLine.Remove(m_cursorPosition - 1, 1);
+ m_cursorPosition--;
+ RefreshLine();
+ }
+ break;
+
+ case ConsoleKey.Insert: // The INS (INSERT) key.
+ m_insertMode = !m_insertMode;
+ RefreshLine();
+ break;
+
+ case ConsoleKey.Delete: // The DEL (DELETE) key.
+ if (m_cursorPosition < activeLineLen) {
+ EnsureNewEntry();
+ m_activeLine.Remove(m_cursorPosition, 1);
+ RefreshLine();
+ }
+ break;
+
+ case ConsoleKey.Enter: // The ENTER key.
+ string newCommand = m_activeLine.ToString();
+
+ if (m_modified) {
+ m_history.Add(m_activeLine);
+ }
+ m_selectedHistory = m_history.Count;
+
+ await Dispatch(newCommand, dispatchCommand);
+
+ SwitchToHistoryEntry();
+ break;
+
+ case ConsoleKey.Escape: // The ESC (ESCAPE) key.
+ EnsureNewEntry();
+ m_activeLine.Clear();
+ m_cursorPosition = 0;
+ RefreshLine();
+ break;
+
+ case ConsoleKey.End: // The END key.
+ m_cursorPosition = activeLineLen;
+ RefreshLine();
+ break;
+
+ case ConsoleKey.Home: // The HOME key.
+ m_cursorPosition = 0;
+ RefreshLine();
+ break;
+
+ case ConsoleKey.LeftArrow: // The LEFT ARROW key.
+ if (keyInfo.Modifiers == ConsoleModifiers.Control) {
+ while (m_cursorPosition > 0 && char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) {
+ m_cursorPosition--;
+ }
+
+ while (m_cursorPosition > 0 && !char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) {
+ m_cursorPosition--;
+ }
+ }
+ else {
+ m_cursorPosition--;
+ }
+
+ RefreshLine();
+ break;
+
+ case ConsoleKey.UpArrow: // The UP ARROW key.
+ if (m_selectedHistory > 0) {
+ m_selectedHistory--;
+ }
+ SwitchToHistoryEntry();
+ break;
+
+ case ConsoleKey.RightArrow: // The RIGHT ARROW key.
+ if (keyInfo.Modifiers == ConsoleModifiers.Control) {
+ while (m_cursorPosition < activeLineLen && !char.IsWhiteSpace(m_activeLine[m_cursorPosition])) {
+ m_cursorPosition++;
+ }
+
+ while (m_cursorPosition < activeLineLen && char.IsWhiteSpace(m_activeLine[m_cursorPosition])) {
+ m_cursorPosition++;
+ }
+ }
+ else {
+ m_cursorPosition++;
+ }
+
+ RefreshLine();
+ break;
+
+ case ConsoleKey.DownArrow: // The DOWN ARROW key.
+ if (m_selectedHistory < m_history.Count) {
+ m_selectedHistory++;
+ }
+ SwitchToHistoryEntry();
+
+ RefreshLine();
+ break;
+
+ default:
+ if (keyInfo.KeyChar != 0) {
+ if ((keyInfo.Modifiers & (ConsoleModifiers.Control | ConsoleModifiers.Alt)) == 0) {
+ AppendNewText(new string(keyInfo.KeyChar, 1));
+ }
+ }
+ break;
+ }
+ }
+
+ private async Task Dispatch(string newCommand, Func<string, CancellationToken, Task> dispatchCommand)
+ {
+ CommandStarting();
+ m_interruptExecutingCommand = new CancellationTokenSource();
+ try
+ {
+ newCommand = newCommand.Trim();
+ if (string.IsNullOrEmpty(newCommand) && m_lastCommandLine != null) {
+ newCommand = m_lastCommandLine;
+ }
+ try
+ {
+ WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand);
+ await dispatchCommand(newCommand, m_interruptExecutingCommand.Token);
+ m_lastCommandLine = newCommand;
+ }
+ catch (OperationCanceledException)
+ {
+ // ctrl-c interrupted the command
+ m_lastCommandLine = null;
+ }
+ catch (Exception ex) when (!(ex is NullReferenceException || ex is ArgumentNullException || ex is ArgumentException))
+ {
+ WriteLine(OutputType.Error, "ERROR: {0}", ex.Message);
+ m_lastCommandLine = null;
+ }
+ }
+ finally
+ {
+ m_interruptExecutingCommand = null;
+ CommandFinished();
+ }
+ }
+
+ private void AppendNewText(string text)
+ {
+ EnsureNewEntry();
+
+ foreach (char c in text) {
+ // Filter unwanted characters.
+ switch (c) {
+ case '\t':
+ case '\r':
+ case '\n':
+ continue;
+ }
+
+ if (m_insertMode && m_cursorPosition < m_activeLine.Length) {
+ m_activeLine[m_cursorPosition] = c;
+ }
+ else {
+ m_activeLine.Insert(m_cursorPosition, c);
+ }
+ m_modified = true;
+ m_cursorPosition++;
+ }
+
+ RefreshLine();
+ }
+
+ private void SwitchToHistoryEntry()
+ {
+ if (m_selectedHistory < m_history.Count) {
+ m_activeLine = m_history[m_selectedHistory];
+ }
+ else {
+ m_activeLine = new StringBuilder();
+ }
+
+ m_cursorPosition = m_activeLine.Length;
+ m_modified = false;
+
+ RefreshLine();
+ }
+
+ private void EnsureNewEntry()
+ {
+ if (!m_modified) {
+ m_activeLine = new StringBuilder(m_activeLine.ToString());
+ m_modified = true;
+ }
+ }
+
+ #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; } }
+
+ class StandardStreamWriter : IStandardStreamWriter
+ {
+ readonly Action<string> _write;
+
+ public StandardStreamWriter(Action<string> write) => _write = write;
+
+ void IStandardStreamWriter.Write(string value) => _write(value);
+ }
+
+ #endregion
+ }
+}
--- /dev/null
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+// --------------------------------------------------------------------
+
+namespace Microsoft.Diagnostics.Repl
+{
+ /// <summary>
+ /// The type of output.
+ /// </summary>
+ public enum OutputType
+ {
+ Normal = 1,
+ Error = 2,
+ Warning = 3,
+ }
+}
--- /dev/null
+<!-- Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <NoWarn>;1591;1701</NoWarn>
+ <Description>Diagnostic utility functions and helpers</Description>
+ <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.CommandLine.Experimental" Version="$(SystemCommandLineExperimentalVersion)" />
+ </ItemGroup>
+
+</Project>
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc;
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Runtime.InteropServices;
-
-namespace Microsoft.Diagnostics.Tools.RuntimeClient
-{
- public static class DiagnosticsHelpers
- {
- /// <summary>
- /// Controls the contents of the dump
- /// </summary>
- public enum DumpType : uint
- {
- Normal = 1,
- WithHeap = 2,
- Triage = 3,
- Full = 4
- }
-
- /// <summary>
- /// Initiate a core dump in the target process runtime.
- /// </summary>
- /// <param name="processId">.NET Core process id</param>
- /// <param name="dumpName">Path and file name of core dump</param>
- /// <param name="dumpType">Type of dump</param>
- /// <param name="diagnostics">If true, log to console the dump generation diagnostics</param>
- /// <returns>DiagnosticsServerErrorCode</returns>
- public static int GenerateCoreDump(int processId, string dumpName, DumpType dumpType, bool diagnostics)
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}");
-
- if (string.IsNullOrEmpty(dumpName))
- throw new ArgumentNullException($"{nameof(dumpName)} required");
-
-
- var payload = SerializeCoreDump(dumpName, dumpType, diagnostics);
- var message = new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload);
-
- var response = IpcClient.SendMessage(processId, message);
-
- var hr = 0;
- switch ((DiagnosticsServerCommandId)response.Header.CommandId)
- {
- case DiagnosticsServerCommandId.Error:
- case DiagnosticsServerCommandId.OK:
- hr = BitConverter.ToInt32(response.Payload);
- break;
- default:
- return -1;
- }
-
- return hr;
- }
-
- /// <summary>
- /// Attach a profiler to the target process runtime.
- /// </summary>
- /// <param name="processId">.NET Core process id</param>
- /// <param name="attachTimeout">The timeout (in ms) for the runtime to wait while attempting to attach.</param>
- /// <param name="profilerGuid">CLSID of the profiler to load</param>
- /// <param name="profilerPath">Path to the profiler library on disk</param>
- /// <param name="additionalData">additional data to pass to the profiler on attach</param>
- /// <returns>HRESULT</returns>
- public static int AttachProfiler(int processId, uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
- {
- if (profilerGuid == null || profilerGuid == Guid.Empty)
- {
- throw new ArgumentException($"{nameof(profilerGuid)} must be a valid Guid");
- }
-
- if (String.IsNullOrEmpty(profilerPath))
- {
- throw new ArgumentException($"{nameof(profilerPath)} must be non-null");
- }
-
- var header = new MessageHeader {
- RequestType = DiagnosticsMessageType.AttachProfiler,
- Pid = (uint)Process.GetCurrentProcess().Id,
- };
-
- byte[] serializedConfiguration = SerializeProfilerAttach(attachTimeout, profilerGuid, profilerPath, additionalData);
- var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration);
-
- var response = IpcClient.SendMessage(processId, message);
-
- var hr = 0;
- switch ((DiagnosticsServerCommandId)response.Header.CommandId)
- {
- case DiagnosticsServerCommandId.Error:
- case DiagnosticsServerCommandId.OK:
- hr = BitConverter.ToInt32(response.Payload);
- break;
- default:
- hr = -1;
- break;
- }
-
- // TODO: the call to set up the pipe and send the message operates on a different timeout than attachTimeout, which is for the runtime.
- // We should eventually have a configurable timeout for the message passing, potentially either separately from the
- // runtime timeout or respect attachTimeout as one total duration.
- return hr;
- }
-
- private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
- {
- using (var stream = new MemoryStream())
- using (var writer = new BinaryWriter(stream))
- {
- writer.Write(attachTimeout);
- writer.Write(profilerGuid.ToByteArray());
- writer.WriteString(profilerPath);
-
- if (additionalData == null)
- {
- writer.Write(0);
- }
- else
- {
- writer.Write(additionalData.Length);
- writer.Write(additionalData);
- }
-
- writer.Flush();
- return stream.ToArray();
- }
- }
-
- private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics)
- {
- using (var stream = new MemoryStream())
- using (var writer = new BinaryWriter(stream))
- {
- writer.WriteString(dumpName);
- writer.Write((uint)dumpType);
- writer.Write((uint)(diagnostics ? 1 : 0));
-
- writer.Flush();
- return stream.ToArray();
- }
- }
- }
-}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient
+{
+ public static class DiagnosticsHelpers
+ {
+ /// <summary>
+ /// Controls the contents of the dump
+ /// </summary>
+ public enum DumpType : uint
+ {
+ Normal = 1,
+ WithHeap = 2,
+ Triage = 3,
+ Full = 4
+ }
+
+ /// <summary>
+ /// Initiate a core dump in the target process runtime.
+ /// </summary>
+ /// <param name="processId">.NET Core process id</param>
+ /// <param name="dumpName">Path and file name of core dump</param>
+ /// <param name="dumpType">Type of dump</param>
+ /// <param name="diagnostics">If true, log to console the dump generation diagnostics</param>
+ /// <returns>DiagnosticsServerErrorCode</returns>
+ public static int GenerateCoreDump(int processId, string dumpName, DumpType dumpType, bool diagnostics)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}");
+
+ if (string.IsNullOrEmpty(dumpName))
+ throw new ArgumentNullException($"{nameof(dumpName)} required");
+
+
+ var payload = SerializeCoreDump(dumpName, dumpType, diagnostics);
+ var message = new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload);
+
+ var response = IpcClient.SendMessage(processId, message);
+
+ var hr = 0;
+ switch ((DiagnosticsServerCommandId)response.Header.CommandId)
+ {
+ case DiagnosticsServerCommandId.Error:
+ case DiagnosticsServerCommandId.OK:
+ hr = BitConverter.ToInt32(response.Payload);
+ break;
+ default:
+ return -1;
+ }
+
+ return hr;
+ }
+
+ /// <summary>
+ /// Attach a profiler to the target process runtime.
+ /// </summary>
+ /// <param name="processId">.NET Core process id</param>
+ /// <param name="attachTimeout">The timeout (in ms) for the runtime to wait while attempting to attach.</param>
+ /// <param name="profilerGuid">CLSID of the profiler to load</param>
+ /// <param name="profilerPath">Path to the profiler library on disk</param>
+ /// <param name="additionalData">additional data to pass to the profiler on attach</param>
+ /// <returns>HRESULT</returns>
+ public static int AttachProfiler(int processId, uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
+ {
+ if (profilerGuid == null || profilerGuid == Guid.Empty)
+ {
+ throw new ArgumentException($"{nameof(profilerGuid)} must be a valid Guid");
+ }
+
+ if (String.IsNullOrEmpty(profilerPath))
+ {
+ throw new ArgumentException($"{nameof(profilerPath)} must be non-null");
+ }
+
+ var header = new MessageHeader {
+ RequestType = DiagnosticsMessageType.AttachProfiler,
+ Pid = (uint)Process.GetCurrentProcess().Id,
+ };
+
+ byte[] serializedConfiguration = SerializeProfilerAttach(attachTimeout, profilerGuid, profilerPath, additionalData);
+ var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration);
+
+ var response = IpcClient.SendMessage(processId, message);
+
+ var hr = 0;
+ switch ((DiagnosticsServerCommandId)response.Header.CommandId)
+ {
+ case DiagnosticsServerCommandId.Error:
+ case DiagnosticsServerCommandId.OK:
+ hr = BitConverter.ToInt32(response.Payload);
+ break;
+ default:
+ hr = -1;
+ break;
+ }
+
+ // TODO: the call to set up the pipe and send the message operates on a different timeout than attachTimeout, which is for the runtime.
+ // We should eventually have a configurable timeout for the message passing, potentially either separately from the
+ // runtime timeout or respect attachTimeout as one total duration.
+ return hr;
+ }
+
+ private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
+ {
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.Write(attachTimeout);
+ writer.Write(profilerGuid.ToByteArray());
+ writer.WriteString(profilerPath);
+
+ if (additionalData == null)
+ {
+ writer.Write(0);
+ }
+ else
+ {
+ writer.Write(additionalData.Length);
+ writer.Write(additionalData);
+ }
+
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+
+ private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics)
+ {
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.WriteString(dumpName);
+ writer.Write((uint)dumpType);
+ writer.Write((uint)(diagnostics ? 1 : 0));
+
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+ }
+}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace Microsoft.Diagnostics.Tools.RuntimeClient
-{
- /// <summary>
- /// Different diagnostic message types that are handled by the runtime.
- /// </summary>
- public enum DiagnosticsMessageType : uint
- {
- /// <summary>
- /// Initiates core dump generation
- /// </summary>
- GenerateCoreDump = 1,
- /// <summary>
- /// Starts an EventPipe session that writes events to a file when the session is stopped or the application exits.
- /// </summary>
- StartEventPipeTracing = 1024,
- /// <summary>
- /// Stops an EventPipe session.
- /// </summary>
- StopEventPipeTracing,
- /// <summary>
- /// Starts an EventPipe session that sends events out-of-proc through IPC.
- /// </summary>
- CollectEventPipeTracing,
- /// <summary>
- /// Attaches a profiler to an existing process
- /// </summary>
- AttachProfiler = 2048,
- }
-}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient
+{
+ /// <summary>
+ /// Different diagnostic message types that are handled by the runtime.
+ /// </summary>
+ public enum DiagnosticsMessageType : uint
+ {
+ /// <summary>
+ /// Initiates core dump generation
+ /// </summary>
+ GenerateCoreDump = 1,
+ /// <summary>
+ /// Starts an EventPipe session that writes events to a file when the session is stopped or the application exits.
+ /// </summary>
+ StartEventPipeTracing = 1024,
+ /// <summary>
+ /// Stops an EventPipe session.
+ /// </summary>
+ StopEventPipeTracing,
+ /// <summary>
+ /// Starts an EventPipe session that sends events out-of-proc through IPC.
+ /// </summary>
+ CollectEventPipeTracing,
+ /// <summary>
+ /// Attaches a profiler to an existing process
+ /// </summary>
+ AttachProfiler = 2048,
+ }
+}
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj" />
+ <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Repl\Microsoft.Diagnostics.Repl.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj" />
</ItemGroup>
-using Microsoft.Diagnostic.Repl;
+using Microsoft.Diagnostics.Repl;
using System.CommandLine;
-namespace Microsoft.Diagnostic.Tools.Dump
+namespace Microsoft.Diagnostics.Tools.Dump
{
[Command(Name = "dumprcw", AliasExpansion = "DumpRCW", Help = "Displays information about a Runtime Callable Wrapper.")]
[Command(Name = "dumpccw", AliasExpansion = "DumpCCW", Help = "Displays information about a COM Callable Wrapper.")]
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj" />
+ <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Repl\Microsoft.Diagnostics.Repl.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\SOS\SOS.Hosting\SOS.Hosting.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\SOS\SOS.NETCore\SOS.NETCore.csproj" />