Add Microsoft.Diagnostic.Repl containing console and command functions.
authorMike McLaughlin <mikem@microsoft.com>
Sun, 17 Feb 2019 20:07:05 +0000 (12:07 -0800)
committerMike McLaughlin <mikem@microsoft.com>
Fri, 22 Feb 2019 23:03:33 +0000 (15:03 -0800)
diagnostics.sln
eng/Versions.props
src/Microsoft.Diagnostic.Repl/Command/Attributes.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Console/OutputType.cs [new file with mode: 0644]
src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj [new file with mode: 0644]

index 03b8f947348c1ff004962434b8e01c19097ffac4..a4f7cac454cfc222798ede524470aa2494585ae8 100644 (file)
@@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.InstallHelper", "src\SO
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sos", "src\Tools\dotnet-sos\dotnet-sos.csproj", "{41351955-16D5-48D7-AF4C-AF25F5FB2E78}"
 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}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Checked|Any CPU = Checked|Any CPU
@@ -593,6 +597,46 @@ Global
                {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
                {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
                {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|Any CPU.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM64.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM64.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x64.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x64.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x86.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x86.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM64.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x64.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x86.ActiveCfg = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x86.Build.0 = Debug|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|Any CPU.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM64.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM64.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x64.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x64.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x86.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x86.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
+               {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
@@ -615,6 +659,7 @@ Global
                {20EBC3C4-917C-402D-B778-9A6E3742BF5A} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
                {1F012743-941B-4915-8C55-02097894CF3F} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
                {41351955-16D5-48D7-AF4C-AF25F5FB2E78} = {B62728C8-1267-4043-B46F-5537BBAEC692}
+               {90CF2633-58F0-44EE-943B-D70207455F20} = {19FAB78C-3351-4911-8F0C-8C6056401740}
        EndGlobalSection
        GlobalSection(ExtensibilityGlobals) = postSolution
                SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0}
index b533cb0d5b8afb8402a54ad96dab377b3436481e..441e3ea30203f899f9c03fc7fca31a75df7efc97 100644 (file)
@@ -29,7 +29,8 @@
       https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json;
       https://dotnet.myget.org/F/symstore/api/v3/index.json;
       https://dotnet.myget.org/F/dotnet-core/api/v3/index.json;
-      https://dotnet.myget.org/F/dotnet-buildtools/api/v3/index.json
+      https://dotnet.myget.org/F/dotnet-buildtools/api/v3/index.json;
+      https://dotnet.myget.org/F/system-commandline/api/v3/index.json
     </RestoreSources>
   </PropertyGroup>
 </Project>
diff --git a/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs b/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs
new file mode 100644 (file)
index 0000000..8083b5e
--- /dev/null
@@ -0,0 +1,61 @@
+// --------------------------------------------------------------------
+// 
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+// 
+// --------------------------------------------------------------------
+using System;
+
+namespace Microsoft.Diagnostic.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>
+    /// 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
+    {
+    }
+}
diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs
new file mode 100644 (file)
index 0000000..7c18f69
--- /dev/null
@@ -0,0 +1,59 @@
+// --------------------------------------------------------------------
+// 
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+// 
+// --------------------------------------------------------------------
+using System;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Repl
+{
+    /// <summary>
+    /// The common command context
+    /// </summary>
+    public abstract class CommandBase
+    {
+        public const string EntryPointName = nameof(InvokeAsync);
+
+        /// <summary>
+        /// Parser invocation context. Contains the ParseResult, CommandResult, etc.
+        /// </summary>
+        public InvocationContext InvocationContext { get; set; }
+
+        /// <summary>
+        /// Console instance
+        /// </summary>
+        public IConsole Console { get { return InvocationContext.Console; } }
+
+        /// <summary>
+        /// The AliasExpansion value from the CommandAttribute or null if none.
+        /// </summary>
+        public string AliasExpansion { get; set; }
+
+        /// <summary>
+        /// Execute the command
+        /// </summary>
+        public abstract Task InvokeAsync();
+
+        /// <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
diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs
new file mode 100644 (file)
index 0000000..9575599
--- /dev/null
@@ -0,0 +1,233 @@
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+//
+// --------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.Repl
+{
+    public class CommandProcessor
+    {
+        private readonly Parser _parser;
+        private readonly Command _rootCommand;
+
+        /// <summary>
+        /// Domain specific context passed to commands
+        /// </summary>
+        public object CommandContext { get; set; }
+
+        /// <summary>
+        /// Create an instance of the command processor;
+        /// </summary>
+        /// <param name="assemblies">The list of assemblies to look for commands</param>
+        public CommandProcessor(IEnumerable<Assembly> assemblies)
+        {
+            var rootBuilder = new CommandLineBuilder();
+            rootBuilder.UseHelp()
+                       .UseParseDirective()
+                       .UseSuggestDirective()
+                       .UseParseErrorReporting()
+                       .UseExceptionHandler();
+            BuildCommands(rootBuilder, assemblies);
+            _rootCommand = rootBuilder.Command;
+            _parser = rootBuilder.Build();
+        }
+
+        /// <summary>
+        /// Parse the command line.
+        /// </summary>
+        /// <param name="commandLine">command line txt</param>
+        /// <param name="console">option console</param>
+        /// <returns>exit code</returns>
+        public Task<int> Parse(string commandLine, IConsole console = null)
+        {
+            ParseResult result = _parser.Parse(commandLine);
+            return _parser.InvokeAsync(result, console);
+        }
+
+        /// <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)
+        {
+            foreach (Type type in assemblies.SelectMany((assembly) => assembly.GetExportedTypes()))
+            {
+                Command command = null;
+
+                var commandAttributes = (CommandAttribute[])type.GetCustomAttributes(typeof(CommandAttribute), inherit: false);
+                foreach (CommandAttribute commandAttribute in commandAttributes)
+                {
+                    // If there is a previous command and the current command doesn't have help or alias expansion, use "simple" aliasing
+                    if (command != null && commandAttribute.Help == null && commandAttribute.AliasExpansion == null) {
+                        command.AddAlias(commandAttribute.Name);
+                        continue;
+                    }
+                    command = new Command(commandAttribute.Name, commandAttribute.Help);
+                    var builder = new CommandLineBuilder(command);
+                    builder.UseHelp();
+
+                    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}");
+                            }
+                            command.Argument = new Argument {
+                                Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(),
+                                Description = argumentAttribute.Help,
+                                ArgumentType = property.PropertyType,
+                                Arity = new ArgumentArity(0, int.MaxValue)
+                            };
+                            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));
+                            }
+                        }
+                    }
+
+                    command.Handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type);
+                    rootBuilder.AddCommand(command);
+                }
+            }
+        }
+
+        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;
+
+            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.GetMethod(CommandBase.EntryPointName, new Type[] { typeof(IHelpBuilder) }) ?? type.GetMethod(CommandBase.EntryPointName) ?? 
+                    throw new ArgumentException($"{CommandBase.EntryPointName} method not found in {type}");
+            }
+
+            public Task<int> InvokeAsync(InvocationContext context)
+            {
+                try
+                {
+                    // Assumes zero parameter constructor
+                    object instance = _constructor.Invoke(new object[0]);
+                    SetProperties(context, instance);
+
+                    var methodBinder = new MethodBinder(_methodInfo, () => instance);
+                    return methodBinder.InvokeAsync(context);
+                }
+                catch (TargetInvocationException ex)
+                {
+                    throw ex.InnerException;
+                }
+            }
+
+            private void SetProperties(InvocationContext context, object instance)
+            {
+                IEnumerable<OptionResult> optionResults = context.ParseResult.CommandResult.Children.OfType<OptionResult>();
+
+                foreach (var 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 (propertyType == typeof(InvocationContext)) {
+                            value = context;
+                        }
+                        else if (propertyType == typeof(IConsole)) {
+                            value = context.Console;
+                        }
+                        else if (propertyType == typeof(CommandProcessor)) {
+                            value = _commandProcessor;
+                        }
+                        else if (propertyType == _commandProcessor.CommandContext?.GetType()) {
+                            value = _commandProcessor.CommandContext;
+                        }
+                        else if (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 (_argument != null)
+                {
+                    object value = context.ParseResult.CommandResult.GetValueOrDefault();
+                    _argument.SetValue(instance, value);
+                }
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs b/src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs
new file mode 100644 (file)
index 0000000..ffb2b3c
--- /dev/null
@@ -0,0 +1,59 @@
+// --------------------------------------------------------------------
+// 
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+// 
+// --------------------------------------------------------------------
+
+using System;
+using System.Text;
+
+namespace Microsoft.Diagnostic.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();
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs b/src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs
new file mode 100644 (file)
index 0000000..d4f9bbd
--- /dev/null
@@ -0,0 +1,487 @@
+// --------------------------------------------------------------------
+// 
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+// 
+// --------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostic.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_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;
+            RefreshLine();
+
+            // Start keyboard processing
+            while (!m_shutdown) {
+                ConsoleKeyInfo keyInfo = Console.ReadKey(true);
+                await ProcessKeyInfo(keyInfo, 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) {
+                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_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_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))
+                {
+                    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
+    }
+}
diff --git a/src/Microsoft.Diagnostic.Repl/Console/OutputType.cs b/src/Microsoft.Diagnostic.Repl/Console/OutputType.cs
new file mode 100644 (file)
index 0000000..f1530ae
--- /dev/null
@@ -0,0 +1,18 @@
+// --------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation.  All rights reserved.
+//
+// --------------------------------------------------------------------
+
+namespace Microsoft.Diagnostic.Repl
+{
+    /// <summary>
+    /// The type of output. 
+    /// </summary>
+    public enum OutputType
+    {
+        Normal      = 1,
+        Error       = 2,
+        Warning     = 3,
+    }
+}
diff --git a/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj b/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj
new file mode 100644 (file)
index 0000000..3fcbc83
--- /dev/null
@@ -0,0 +1,17 @@
+<!-- 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>
+    <DebugType>embedded</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <PackageReference Include="System.CommandLine.Experimental" Version="0.1.0-alpha-63807-01" />
+  </ItemGroup>
+  
+</Project>