Move command service impl to M.D.DS.Implementation (#2772)
authorMike McLaughlin <mikem@microsoft.com>
Wed, 8 Dec 2021 06:44:40 +0000 (22:44 -0800)
committerGitHub <noreply@github.com>
Wed, 8 Dec 2021 06:44:40 +0000 (06:44 +0000)
* Move command service impl to M.D.DS.Implementation

Rename to CommandService. The lldb/dbgeng SOS doesn't need the repl. One less assembly in that package.

Clean up console service/repl assembly. Rename to ConsoleService.

* Code review feedback

18 files changed:
src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices.Implementation/Microsoft.Diagnostics.DebugServices.Implementation.csproj
src/Microsoft.Diagnostics.DebugServices/ICommandService.cs
src/Microsoft.Diagnostics.Repl/CharToLineConverter.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Repl/Command/CommandProcessor.cs [deleted file]
src/Microsoft.Diagnostics.Repl/Command/HelpCommand.cs [deleted file]
src/Microsoft.Diagnostics.Repl/Console/CharToLineConverter.cs [deleted file]
src/Microsoft.Diagnostics.Repl/Console/ConsoleProvider.cs [deleted file]
src/Microsoft.Diagnostics.Repl/Console/ExitCommand.cs [deleted file]
src/Microsoft.Diagnostics.Repl/Console/OutputType.cs [deleted file]
src/Microsoft.Diagnostics.Repl/ConsoleService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Repl/ExitCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Repl/HelpCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Repl/Microsoft.Diagnostics.Repl.csproj
src/Microsoft.Diagnostics.Repl/OutputType.cs [new file with mode: 0644]
src/SOS/SOS.Extensions/HostServices.cs
src/SOS/SOS.Extensions/SOS.Extensions.csproj
src/Tools/dotnet-dump/Analyzer.cs

diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs
new file mode 100644 (file)
index 0000000..60fdbbb
--- /dev/null
@@ -0,0 +1,562 @@
+// 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 System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Help;
+using System.CommandLine.Invocation;
+using System.CommandLine.IO;
+using System.CommandLine.Parsing;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.DebugServices.Implementation
+{
+    /// <summary>
+    /// Implements the ICommandService interface using System.CommandLine.
+    /// </summary>
+    public class CommandService : ICommandService
+    {
+        private Parser _parser;
+        private readonly CommandLineBuilder _rootBuilder;
+        private readonly Dictionary<string, CommandHandler> _commandHandlers = new Dictionary<string, CommandHandler>();
+
+        /// <summary>
+        /// Create an instance of the command processor;
+        /// </summary>
+        /// <param name="commandPrompt">command prompted used in help message</param>
+        public CommandService(string commandPrompt = null)
+        {
+            _rootBuilder = new CommandLineBuilder(new Command(commandPrompt ?? ">"));
+            _rootBuilder.UseHelpBuilder((bindingContext) => new LocalHelpBuilder(this, bindingContext.Console, useHelpBuilder: false));
+        }
+
+        /// <summary>
+        /// Parse and execute the command line.
+        /// </summary>
+        /// <param name="commandLine">command line text</param>
+        /// <param name="services">services for the command</param>
+        /// <returns>exit code</returns>
+        public int Execute(string commandLine, IServiceProvider services)
+        {
+            // Parse the command line and invoke the command
+            ParseResult parseResult = Parser.Parse(commandLine);
+
+            var context = new InvocationContext(parseResult, new LocalConsole(services));
+            if (parseResult.Errors.Count > 0)
+            {
+                context.InvocationResult = new ParseErrorResult();
+            }
+            else
+            {
+                if (parseResult.CommandResult.Command is Command command)
+                {
+                    if (command.Handler is CommandHandler handler)
+                    {
+                        ITarget target = services.GetService<ITarget>();
+                        if (!handler.IsValidPlatform(target))
+                        { 
+                            if (target != null)
+                            {
+                                context.Console.Error.WriteLine($"Command '{command.Name}' not supported on this target");
+                            }
+                            else
+                            {
+                                context.Console.Error.WriteLine($"Command '{command.Name}' needs a target");
+                            }
+                            return 1;
+                        }
+                        try
+                        {
+                            handler.Invoke(context, services);
+                        }
+                        catch (Exception ex)
+                        {
+                            OnException(ex, context);
+                        }
+                    }
+                }
+            }
+
+            context.InvocationResult?.Apply(context);
+            return context.ResultCode;
+        }
+
+        /// <summary>
+        /// Displays the help for a command
+        /// </summary>
+        /// <param name="commandName">name of the command or alias</param>
+        /// <param name="services">service provider</param>
+        /// <returns>true if success, false if command not found</returns>
+        public bool DisplayHelp(string commandName, IServiceProvider services)
+        {
+            Command command = null;
+            if (!string.IsNullOrEmpty(commandName)) 
+            {
+                command = _rootBuilder.Command.Children.OfType<Command>().FirstOrDefault((cmd) => commandName == cmd.Name || cmd.Aliases.Any((alias) => commandName == alias));
+                if (command == null)
+                {
+                    return false;
+                }
+                if (command.Handler is CommandHandler handler)
+                {
+                    ITarget target = services.GetService<ITarget>();
+                    if (!handler.IsValidPlatform(target))
+                    {
+                        return false;
+                    }
+                }
+            }
+            else 
+            {
+                ITarget target = services.GetService<ITarget>();
+
+                // Create temporary builder adding only the commands that are valid for the target
+                var builder = new CommandLineBuilder(new Command(_rootBuilder.Command.Name));
+                foreach (Command cmd in _rootBuilder.Command.Children.OfType<Command>())
+                {
+                    if (cmd.Handler is CommandHandler handler)
+                    {
+                        if (handler.IsValidPlatform(target))
+                        {
+                            builder.AddCommand(cmd);
+                        }
+                    }
+                }
+                command = builder.Command;
+            }
+            Debug.Assert(command != null);
+            IHelpBuilder helpBuilder = new LocalHelpBuilder(this, new LocalConsole(services), useHelpBuilder: true);
+            helpBuilder.Write(command);
+            return true;
+        }
+
+        /// <summary>
+        /// Enumerates all the command's name and help
+        /// </summary>
+        public IEnumerable<(string name, string help, IEnumerable<string> aliases)> Commands => _commandHandlers.Select((keypair) => (keypair.Value.Name, keypair.Value.Help, keypair.Value.Aliases));
+
+        /// <summary>
+        /// Add the commands and aliases attributes found in the type.
+        /// </summary>
+        /// <param name="type">Command type to search</param>
+        /// <param name="factory">function to create command instance</param>
+        public void AddCommands(Type type, Func<IServiceProvider, object> factory)
+        {
+            for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
+            {
+                if (baseType == typeof(CommandBase)) {
+                    break;
+                }
+                var commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false);
+                foreach (CommandAttribute commandAttribute in commandAttributes)
+                {
+                    if (factory == null)
+                    {
+                        // Assumes zero parameter constructor
+                        ConstructorInfo constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ??
+                            throw new ArgumentException($"No eligible constructor found in {type}");
+
+                        factory = (services) => constructor.Invoke(Array.Empty<object>());
+                    }
+                    CreateCommand(baseType, commandAttribute, factory);
+                }
+            }
+
+            // Build or re-build parser instance after all these commands and aliases are added
+            FlushParser();
+        }
+
+        private void CreateCommand(Type type, CommandAttribute commandAttribute, Func<IServiceProvider, object> factory)
+        {
+            var command = new Command(commandAttribute.Name, commandAttribute.Help);
+            var properties = new List<(PropertyInfo, Option)>();
+            var arguments = new List<(PropertyInfo, Argument)>();
+
+            foreach (string alias in commandAttribute.Aliases)
+            {
+                command.AddAlias(alias);
+            }
+
+            foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite))
+            {
+                var argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault();
+                if (argumentAttribute != null)
+                {
+                    IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne;
+
+                    var argument = new Argument {
+                        Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(),
+                        Description = argumentAttribute.Help,
+                        ArgumentType = property.PropertyType,
+                        Arity = arity
+                    };
+                    command.AddArgument(argument);
+                    arguments.Add((property, argument));
+                }
+                else
+                {
+                    var optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault();
+                    if (optionAttribute != null)
+                    {
+                        var option = new Option(optionAttribute.Name ?? BuildOptionAlias(property.Name), optionAttribute.Help) {
+                            Argument = new Argument { ArgumentType = property.PropertyType }
+                        };
+                        command.AddOption(option);
+                        properties.Add((property, option));
+
+                        foreach (string alias in optionAttribute.Aliases)
+                        {
+                            option.AddAlias(alias);
+                        }
+                    }
+                    else
+                    {
+                        // If not an option, add as just a settable properties
+                        properties.Add((property, null));
+                    }
+                }
+            }
+
+            var handler = new CommandHandler(commandAttribute, arguments, properties, type, factory);
+            _commandHandlers.Add(command.Name, handler);
+            command.Handler = handler;
+            _rootBuilder.AddCommand(command);
+        }
+
+        private Parser Parser => _parser ??= _rootBuilder.Build();
+
+        private void FlushParser() => _parser = null;
+
+        private void OnException(Exception ex, InvocationContext context)
+        {
+            if (ex is TargetInvocationException)
+            {
+                ex = ex.InnerException;
+            }
+            if (ex is NullReferenceException || 
+                ex is ArgumentException || 
+                ex is ArgumentNullException || 
+                ex is ArgumentOutOfRangeException || 
+                ex is NotImplementedException) 
+            {
+                context.Console.Error.WriteLine(ex.ToString());
+            }
+            else 
+            {
+                context.Console.Error.WriteLine(ex.Message);
+            }
+            Trace.TraceError(ex.ToString());
+        }
+
+        private static string BuildOptionAlias(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()}";
+        }
+
+        /// <summary>
+        /// The normal command handler.
+        /// </summary>
+        class CommandHandler : ICommandHandler
+        {
+            private readonly CommandAttribute _commandAttribute;
+            private readonly IEnumerable<(PropertyInfo Property, Argument Argument)> _arguments;
+            private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties;
+
+            private readonly Func<IServiceProvider, object> _factory;
+            private readonly MethodInfo _methodInfo;
+            private readonly MethodInfo _methodInfoHelp;
+
+            public CommandHandler(
+                CommandAttribute commandAttribute,
+                IEnumerable<(PropertyInfo, Argument)> arguments, 
+                IEnumerable<(PropertyInfo, Option)> properties, 
+                Type type, 
+                Func<IServiceProvider, object> factory)
+            {
+                _commandAttribute = commandAttribute;
+                _arguments = arguments;
+                _properties = properties;
+                _factory = factory;
+
+                _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)
+            {
+                return Task.FromException<int>(new NotImplementedException());
+            }
+
+            /// <summary>
+            /// Returns the command name
+            /// </summary>
+            internal string Name => _commandAttribute.Name;
+
+            /// <summary>
+            /// Returns the command's help text
+            /// </summary>
+            internal string Help => _commandAttribute.Help;
+
+            /// <summary>
+            /// Returns the list of the command's aliases.
+            /// </summary>
+            internal IEnumerable<string> Aliases => _commandAttribute.Aliases;
+
+            /// <summary>
+            /// Returns true if the command should be added.
+            /// </summary>
+            internal bool IsValidPlatform(ITarget target)
+            {
+                if ((_commandAttribute.Platform & CommandPlatform.Global) != 0)
+                {
+                    return true;
+                }
+                if (target != null)
+                {
+                    if (target.OperatingSystem == OSPlatform.Windows)
+                    {
+                        return (_commandAttribute.Platform & CommandPlatform.Windows) != 0;
+                    }
+                    if (target.OperatingSystem == OSPlatform.Linux)
+                    {
+                        return (_commandAttribute.Platform & CommandPlatform.Linux) != 0;
+                    }
+                    if (target.OperatingSystem == OSPlatform.OSX)
+                    {
+                        return (_commandAttribute.Platform & CommandPlatform.OSX) != 0;
+                    }
+                }
+                return false;
+            }
+
+            /// <summary>
+            /// Execute the command synchronously.
+            /// </summary>
+            /// <param name="context">invocation context</param>
+            /// <param name="services">service provider</param>
+            internal void Invoke(InvocationContext context, IServiceProvider services) => Invoke(_methodInfo, context, context.Parser, services);
+
+            /// <summary>
+            /// Executes the command's help invoke function if exists
+            /// </summary>
+            /// <param name="parser">parser instance</param>
+            /// <param name="services">service provider</param>
+            /// <returns>true help called, false no help function</returns>
+            internal bool InvokeHelp(Parser parser, IServiceProvider services)
+            {
+                if (_methodInfoHelp == null) {
+                    return false;
+                }
+                // The InvocationContext is null so the options and arguments in the 
+                // command instance created are not 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, parser, services);
+                return true;
+            }
+
+            private void Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services)
+            {
+                object instance = _factory(services);
+                SetProperties(context, parser, services, instance);
+
+                object[] arguments = BuildArguments(methodInfo, services);
+                methodInfo.Invoke(instance, arguments);
+            }
+
+            private void SetProperties(InvocationContext context, Parser parser, IServiceProvider services, object instance)
+            {
+                ParseResult defaultParseResult = null;
+
+                // Parse the default options if any
+                string defaultOptions = _commandAttribute.DefaultOptions;
+                if (defaultOptions != null) {
+                    defaultParseResult = parser.Parse(Name + " " + defaultOptions);
+                }
+
+                // Now initialize the option and service properties from the default and command line options
+                foreach ((PropertyInfo Property, Option Option) property in _properties)
+                {
+                    object value = property.Property.GetValue(instance);
+
+                    if (property.Option != null)
+                    {
+                        if (defaultParseResult != null)
+                        {
+                            OptionResult defaultOptionResult = defaultParseResult.FindResultFor(property.Option);
+                            if (defaultOptionResult != null) {
+                                value = defaultOptionResult.GetValueOrDefault();
+                            }
+                        }
+                        if (context != null)
+                        {
+                            OptionResult optionResult = context.ParseResult.FindResultFor(property.Option);
+                            if (optionResult != null) {
+                                value = optionResult.GetValueOrDefault();
+                            }
+                        }
+                    }
+                    else
+                    { 
+                        Type propertyType = property.Property.PropertyType;
+                        object service = services.GetService(propertyType);
+                        if (service != null) {
+                            value = service;
+                        }
+                    }
+
+                    property.Property.SetValue(instance, value);
+                }
+
+                // Initialize any argument properties from the default and command line arguments
+                foreach ((PropertyInfo Property, Argument Argument) argument in _arguments)
+                {
+                    object value = argument.Property.GetValue(instance);
+
+                    List<string> array = null;
+                    if (argument.Property.PropertyType.IsArray && argument.Property.PropertyType.GetElementType() == typeof(string))
+                    {
+                        array = new List<string>();
+                        if (value is IEnumerable<string> entries) {
+                            array.AddRange(entries);
+                        }
+                    }
+
+                    if (defaultParseResult != null)
+                    {
+                        ArgumentResult defaultArgumentResult = defaultParseResult.FindResultFor(argument.Argument);
+                        if (defaultArgumentResult != null)
+                        {
+                            value = defaultArgumentResult.GetValueOrDefault();
+                            if (array != null && value is IEnumerable<string> entries) {
+                                array.AddRange(entries);
+                            }
+                        }
+                    }
+                    if (context != null)
+                    {
+                        ArgumentResult argumentResult = context.ParseResult.FindResultFor(argument.Argument);
+                        if (argumentResult != null) 
+                        {
+                            value = argumentResult.GetValueOrDefault();
+                            if (array != null && value is IEnumerable<string> entries) {
+                                array.AddRange(entries);
+                            }
+                        }
+                    }
+
+                    argument.Property.SetValue(instance, array != null ? array.ToArray() : value);
+                }
+            }
+
+            private object[] BuildArguments(MethodBase methodBase, IServiceProvider services)
+            {
+                ParameterInfo[] parameters = methodBase.GetParameters();
+                object[] arguments = new object[parameters.Length];
+                for (int i = 0; i < parameters.Length; i++)
+                {
+                    Type parameterType = parameters[i].ParameterType;
+
+                    // The parameter will passed as null to allow for "optional" services. The invoked 
+                    // method needs to check for possible null parameters.
+                    arguments[i] = services.GetService(parameterType);
+                }
+                return arguments;
+            }
+        }
+
+        /// <summary>
+        /// Local help builder that allows commands to provide more detailed help 
+        /// text via the "InvokeHelp" function.
+        /// </summary>
+        class LocalHelpBuilder : IHelpBuilder
+        {
+            private readonly CommandService _commandService;
+            private readonly IConsole _console;
+            private readonly bool _useHelpBuilder;
+
+            public LocalHelpBuilder(CommandService commandService, IConsole console, bool useHelpBuilder)
+            {
+                _commandService = commandService;
+                _console = console;
+                _useHelpBuilder = useHelpBuilder;
+            }
+
+            void IHelpBuilder.Write(ICommand command)
+            {
+                bool useHelpBuilder = _useHelpBuilder;
+                if (_commandService._commandHandlers.TryGetValue(command.Name, out CommandHandler handler))
+                {
+                    if (handler.InvokeHelp(_commandService.Parser, LocalConsole.ToServices(_console))) {
+                        return;
+                    }
+                    useHelpBuilder = true;
+                }
+                if (useHelpBuilder)
+                {
+                    var helpBuilder = new HelpBuilder(_console, maxWidth: Console.WindowWidth);
+                    helpBuilder.Write(command);
+                }
+            }
+        }
+
+        /// <summary>
+        /// This class does two things: wraps the IConsoleService and provides the IConsole interface and 
+        /// pipes through the System.CommandLine parsing allowing per command invocation data (service 
+        /// provider and raw command line) to be passed through.
+        /// </summary>
+        class LocalConsole : IConsole
+        {
+            public static IServiceProvider ToServices(IConsole console) => ((LocalConsole)console).Services;
+
+            public readonly IServiceProvider Services;
+
+            private readonly IConsoleService _console;
+
+            public LocalConsole(IServiceProvider services)
+            {
+                Services = services;
+                _console = services.GetService<IConsoleService>();
+                Debug.Assert(_console != null);
+                Out = new StandardStreamWriter((text) => _console.Write(text));
+                Error = new StandardStreamWriter((text) => _console.WriteError(text));
+            }
+
+            #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
+        }
+    }
+}
index bb0e434295d2f9956d9676023bec4caebd1cff22..a7ede1880dd8f3a8e83d3d04a5c3b34d9671cc71 100644 (file)
@@ -17,6 +17,7 @@
     <PackageReference Include="Microsoft.Diagnostics.Runtime" Version="$(MicrosoftDiagnosticsRuntimeVersion)" />
     <PackageReference Include="Microsoft.SymbolStore" Version="$(MicrosoftSymbolStoreVersion)" />
     <PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
+    <PackageReference Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
     <PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" />
   </ItemGroup>
   
index 8f37d47c6580d69dd2941f5555aeab80d370c3bb..7b46438428b64a2ecca435ccfb9dcf871738b576 100644 (file)
@@ -23,5 +23,13 @@ namespace Microsoft.Diagnostics.DebugServices
         /// <param name="type">Command type to search</param>
         /// <param name="factory">function to create command instance</param>
         void AddCommands(Type type, Func<IServiceProvider, object> factory);
+
+        /// <summary>
+        /// Displays the help for a command
+        /// </summary>
+        /// <param name="commandName">name of the command or alias</param>
+        /// <param name="services">service provider</param>
+        /// <returns>true if success, false if command not found</returns>
+        bool DisplayHelp(string commandName, IServiceProvider services);
     }
 }
diff --git a/src/Microsoft.Diagnostics.Repl/CharToLineConverter.cs b/src/Microsoft.Diagnostics.Repl/CharToLineConverter.cs
new file mode 100644 (file)
index 0000000..323b27b
--- /dev/null
@@ -0,0 +1,57 @@
+// 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 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();
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Repl/Command/CommandProcessor.cs b/src/Microsoft.Diagnostics.Repl/Command/CommandProcessor.cs
deleted file mode 100644 (file)
index cce0c9a..0000000
+++ /dev/null
@@ -1,566 +0,0 @@
-// 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.DebugServices;
-using System;
-using System.Collections.Generic;
-using System.CommandLine;
-using System.CommandLine.Builder;
-using System.CommandLine.Help;
-using System.CommandLine.Invocation;
-using System.CommandLine.IO;
-using System.CommandLine.Parsing;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Repl
-{
-    /// <summary>
-    /// Implements the ICommandService interface using System.CommandLine.
-    /// </summary>
-    public class CommandProcessor : ICommandService
-    {
-        private Parser _parser;
-        private readonly CommandLineBuilder _rootBuilder;
-        private readonly Dictionary<string, CommandHandler> _commandHandlers = new Dictionary<string, CommandHandler>();
-
-        /// <summary>
-        /// Create an instance of the command processor;
-        /// </summary>
-        /// <param name="commandPrompt">command prompted used in help message</param>
-        public CommandProcessor(string commandPrompt = null)
-        {
-            _rootBuilder = new CommandLineBuilder(new Command(commandPrompt ?? ">"));
-            _rootBuilder.UseHelpBuilder((bindingContext) => new LocalHelpBuilder(this, bindingContext.Console, useHelpBuilder: false));
-        }
-
-        /// <summary>
-        /// Parse and execute the command line.
-        /// </summary>
-        /// <param name="commandLine">command line text</param>
-        /// <param name="services">services for the command</param>
-        /// <returns>exit code</returns>
-        public int Execute(string commandLine, IServiceProvider services)
-        {
-            Debug.Assert(_parser != null);
-
-            // Parse the command line and invoke the command
-            ParseResult parseResult = _parser.Parse(commandLine);
-
-            var context = new InvocationContext(parseResult, new LocalConsole(services));
-            if (parseResult.Errors.Count > 0)
-            {
-                context.InvocationResult = new ParseErrorResult();
-            }
-            else
-            {
-                if (parseResult.CommandResult.Command is Command command)
-                {
-                    if (command.Handler is CommandHandler handler)
-                    {
-                        ITarget target = services.GetService<ITarget>();
-                        if (!handler.IsValidPlatform(target))
-                        { 
-                            if (target != null)
-                            {
-                                context.Console.Error.WriteLine($"Command '{command.Name}' not supported on this target");
-                            }
-                            else
-                            {
-                                context.Console.Error.WriteLine($"Command '{command.Name}' needs a target");
-                            }
-                            return 1;
-                        }
-                        try
-                        {
-                            handler.Invoke(context, services);
-                        }
-                        catch (Exception ex)
-                        {
-                            OnException(ex, context);
-                        }
-                    }
-                }
-            }
-
-            context.InvocationResult?.Apply(context);
-            return context.ResultCode;
-        }
-
-        /// <summary>
-        /// Displays the help for a command
-        /// </summary>
-        /// <param name="commandName">name of the command or alias</param>
-        /// <param name="services">service provider</param>
-        /// <returns>true if success, false if command not found</returns>
-        public bool DisplayHelp(string commandName, IServiceProvider services)
-        {
-            Command command = null;
-            if (!string.IsNullOrEmpty(commandName)) 
-            {
-                command = _rootBuilder.Command.Children.OfType<Command>().FirstOrDefault((cmd) => commandName == cmd.Name || cmd.Aliases.Any((alias) => commandName == alias));
-                if (command == null)
-                {
-                    return false;
-                }
-                if (command.Handler is CommandHandler handler)
-                {
-                    ITarget target = services.GetService<ITarget>();
-                    if (!handler.IsValidPlatform(target))
-                    {
-                        return false;
-                    }
-                }
-            }
-            else 
-            {
-                ITarget target = services.GetService<ITarget>();
-
-                // Create temporary builder adding only the commands that are valid for the target
-                var builder = new CommandLineBuilder(new Command(_rootBuilder.Command.Name));
-                foreach (Command cmd in _rootBuilder.Command.Children.OfType<Command>())
-                {
-                    if (cmd.Handler is CommandHandler handler)
-                    {
-                        if (handler.IsValidPlatform(target))
-                        {
-                            builder.AddCommand(cmd);
-                        }
-                    }
-                }
-                command = builder.Command;
-            }
-            Debug.Assert(command != null);
-            IHelpBuilder helpBuilder = new LocalHelpBuilder(this, new LocalConsole(services), useHelpBuilder: true);
-            helpBuilder.Write(command);
-            return true;
-        }
-
-        /// <summary>
-        /// Enumerates all the command's name and help
-        /// </summary>
-        public IEnumerable<(string name, string help, IEnumerable<string> aliases)> Commands => _commandHandlers.Select((keypair) => (keypair.Value.Name, keypair.Value.Help, keypair.Value.Aliases));
-
-        /// <summary>
-        /// Add the commands and aliases attributes found in the type.
-        /// </summary>
-        /// <param name="type">Command type to search</param>
-        /// <param name="factory">function to create command instance</param>
-        public void AddCommands(Type type, Func<IServiceProvider, object> factory)
-        {
-            for (Type baseType = type; baseType != null; baseType = baseType.BaseType)
-            {
-                if (baseType == typeof(CommandBase)) {
-                    break;
-                }
-                var commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false);
-                foreach (CommandAttribute commandAttribute in commandAttributes)
-                {
-                    if (factory == null)
-                    {
-                        // Assumes zero parameter constructor
-                        ConstructorInfo constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ??
-                            throw new ArgumentException($"No eligible constructor found in {type}");
-
-                        factory = (services) => constructor.Invoke(Array.Empty<object>());
-                    }
-                    CreateCommand(baseType, commandAttribute, factory);
-                }
-            }
-
-            // Build parser instance after all the commands and aliases are added
-            BuildParser();
-        }
-
-        private void CreateCommand(Type type, CommandAttribute commandAttribute, Func<IServiceProvider, object> factory)
-        {
-            var command = new Command(commandAttribute.Name, commandAttribute.Help);
-            var properties = new List<(PropertyInfo, Option)>();
-            var arguments = new List<(PropertyInfo, Argument)>();
-
-            foreach (string alias in commandAttribute.Aliases)
-            {
-                command.AddAlias(alias);
-            }
-
-            foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite))
-            {
-                var argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault();
-                if (argumentAttribute != null)
-                {
-                    IArgumentArity arity = property.PropertyType.IsArray ? ArgumentArity.ZeroOrMore : ArgumentArity.ZeroOrOne;
-
-                    var argument = new Argument {
-                        Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(),
-                        Description = argumentAttribute.Help,
-                        ArgumentType = property.PropertyType,
-                        Arity = arity
-                    };
-                    command.AddArgument(argument);
-                    arguments.Add((property, argument));
-                }
-                else
-                {
-                    var optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault();
-                    if (optionAttribute != null)
-                    {
-                        var option = new Option(optionAttribute.Name ?? BuildOptionAlias(property.Name), optionAttribute.Help) {
-                            Argument = new Argument { ArgumentType = property.PropertyType }
-                        };
-                        command.AddOption(option);
-                        properties.Add((property, option));
-
-                        foreach (string alias in optionAttribute.Aliases)
-                        {
-                            option.AddAlias(alias);
-                        }
-                    }
-                    else
-                    {
-                        // If not an option, add as just a settable properties
-                        properties.Add((property, null));
-                    }
-                }
-            }
-
-            var handler = new CommandHandler(commandAttribute, arguments, properties, type, factory);
-            _commandHandlers.Add(command.Name, handler);
-            command.Handler = handler;
-            _rootBuilder.AddCommand(command);
-        }
-
-        private void BuildParser()
-        {
-            _parser = _rootBuilder.Build();
-        }
-
-        private void OnException(Exception ex, InvocationContext context)
-        {
-            if (ex is TargetInvocationException)
-            {
-                ex = ex.InnerException;
-            }
-            if (ex is NullReferenceException || 
-                ex is ArgumentException || 
-                ex is ArgumentNullException || 
-                ex is ArgumentOutOfRangeException || 
-                ex is NotImplementedException) 
-            {
-                context.Console.Error.WriteLine(ex.ToString());
-            }
-            else 
-            {
-                context.Console.Error.WriteLine(ex.Message);
-            }
-            Trace.TraceError(ex.ToString());
-        }
-
-        private static string BuildOptionAlias(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()}";
-        }
-
-        /// <summary>
-        /// The normal command handler.
-        /// </summary>
-        class CommandHandler : ICommandHandler
-        {
-            private readonly CommandAttribute _commandAttribute;
-            private readonly IEnumerable<(PropertyInfo Property, Argument Argument)> _arguments;
-            private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties;
-
-            private readonly Func<IServiceProvider, object> _factory;
-            private readonly MethodInfo _methodInfo;
-            private readonly MethodInfo _methodInfoHelp;
-
-            public CommandHandler(
-                CommandAttribute commandAttribute,
-                IEnumerable<(PropertyInfo, Argument)> arguments, 
-                IEnumerable<(PropertyInfo, Option)> properties, 
-                Type type, 
-                Func<IServiceProvider, object> factory)
-            {
-                _commandAttribute = commandAttribute;
-                _arguments = arguments;
-                _properties = properties;
-                _factory = factory;
-
-                _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)
-            {
-                return Task.FromException<int>(new NotImplementedException());
-            }
-
-            /// <summary>
-            /// Returns the command name
-            /// </summary>
-            internal string Name => _commandAttribute.Name;
-
-            /// <summary>
-            /// Returns the command's help text
-            /// </summary>
-            internal string Help => _commandAttribute.Help;
-
-            /// <summary>
-            /// Returns the list of the command's aliases.
-            /// </summary>
-            internal IEnumerable<string> Aliases => _commandAttribute.Aliases;
-
-            /// <summary>
-            /// Returns true if the command should be added.
-            /// </summary>
-            internal bool IsValidPlatform(ITarget target)
-            {
-                if ((_commandAttribute.Platform & CommandPlatform.Global) != 0)
-                {
-                    return true;
-                }
-                if (target != null)
-                {
-                    if (target.OperatingSystem == OSPlatform.Windows)
-                    {
-                        return (_commandAttribute.Platform & CommandPlatform.Windows) != 0;
-                    }
-                    if (target.OperatingSystem == OSPlatform.Linux)
-                    {
-                        return (_commandAttribute.Platform & CommandPlatform.Linux) != 0;
-                    }
-                    if (target.OperatingSystem == OSPlatform.OSX)
-                    {
-                        return (_commandAttribute.Platform & CommandPlatform.OSX) != 0;
-                    }
-                }
-                return false;
-            }
-
-            /// <summary>
-            /// Execute the command synchronously.
-            /// </summary>
-            /// <param name="context">invocation context</param>
-            /// <param name="services">service provider</param>
-            internal void Invoke(InvocationContext context, IServiceProvider services) => Invoke(_methodInfo, context, context.Parser, services);
-
-            /// <summary>
-            /// Executes the command's help invoke function if exists
-            /// </summary>
-            /// <param name="parser">parser instance</param>
-            /// <param name="services">service provider</param>
-            /// <returns>true help called, false no help function</returns>
-            internal bool InvokeHelp(Parser parser, IServiceProvider services)
-            {
-                if (_methodInfoHelp == null) {
-                    return false;
-                }
-                // The InvocationContext is null so the options and arguments in the 
-                // command instance created are not 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, parser, services);
-                return true;
-            }
-
-            private void Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services)
-            {
-                object instance = _factory(services);
-                SetProperties(context, parser, services, instance);
-
-                object[] arguments = BuildArguments(methodInfo, services);
-                methodInfo.Invoke(instance, arguments);
-            }
-
-            private void SetProperties(InvocationContext context, Parser parser, IServiceProvider services, object instance)
-            {
-                ParseResult defaultParseResult = null;
-
-                // Parse the default options if any
-                string defaultOptions = _commandAttribute.DefaultOptions;
-                if (defaultOptions != null) {
-                    defaultParseResult = parser.Parse(Name + " " + defaultOptions);
-                }
-
-                // Now initialize the option and service properties from the default and command line options
-                foreach ((PropertyInfo Property, Option Option) property in _properties)
-                {
-                    object value = property.Property.GetValue(instance);
-
-                    if (property.Option != null)
-                    {
-                        if (defaultParseResult != null)
-                        {
-                            OptionResult defaultOptionResult = defaultParseResult.FindResultFor(property.Option);
-                            if (defaultOptionResult != null) {
-                                value = defaultOptionResult.GetValueOrDefault();
-                            }
-                        }
-                        if (context != null)
-                        {
-                            OptionResult optionResult = context.ParseResult.FindResultFor(property.Option);
-                            if (optionResult != null) {
-                                value = optionResult.GetValueOrDefault();
-                            }
-                        }
-                    }
-                    else
-                    { 
-                        Type propertyType = property.Property.PropertyType;
-                        object service = services.GetService(propertyType);
-                        if (service != null) {
-                            value = service;
-                        }
-                    }
-
-                    property.Property.SetValue(instance, value);
-                }
-
-                // Initialize any argument properties from the default and command line arguments
-                foreach ((PropertyInfo Property, Argument Argument) argument in _arguments)
-                {
-                    object value = argument.Property.GetValue(instance);
-
-                    List<string> array = null;
-                    if (argument.Property.PropertyType.IsArray && argument.Property.PropertyType.GetElementType() == typeof(string))
-                    {
-                        array = new List<string>();
-                        if (value is IEnumerable<string> entries) {
-                            array.AddRange(entries);
-                        }
-                    }
-
-                    if (defaultParseResult != null)
-                    {
-                        ArgumentResult defaultArgumentResult = defaultParseResult.FindResultFor(argument.Argument);
-                        if (defaultArgumentResult != null)
-                        {
-                            value = defaultArgumentResult.GetValueOrDefault();
-                            if (array != null && value is IEnumerable<string> entries) {
-                                array.AddRange(entries);
-                            }
-                        }
-                    }
-                    if (context != null)
-                    {
-                        ArgumentResult argumentResult = context.ParseResult.FindResultFor(argument.Argument);
-                        if (argumentResult != null) 
-                        {
-                            value = argumentResult.GetValueOrDefault();
-                            if (array != null && value is IEnumerable<string> entries) {
-                                array.AddRange(entries);
-                            }
-                        }
-                    }
-
-                    argument.Property.SetValue(instance, array != null ? array.ToArray() : value);
-                }
-            }
-
-            private object[] BuildArguments(MethodBase methodBase, IServiceProvider services)
-            {
-                ParameterInfo[] parameters = methodBase.GetParameters();
-                object[] arguments = new object[parameters.Length];
-                for (int i = 0; i < parameters.Length; i++)
-                {
-                    Type parameterType = parameters[i].ParameterType;
-
-                    // The parameter will passed as null to allow for "optional" services. The invoked 
-                    // method needs to check for possible null parameters.
-                    arguments[i] = services.GetService(parameterType);
-                }
-                return arguments;
-            }
-        }
-
-        /// <summary>
-        /// Local help builder that allows commands to provide more detailed help 
-        /// text via the "InvokeHelp" function.
-        /// </summary>
-        class LocalHelpBuilder : IHelpBuilder
-        {
-            private readonly CommandProcessor _commandProcessor;
-            private readonly IConsole _console;
-            private readonly bool _useHelpBuilder;
-
-            public LocalHelpBuilder(CommandProcessor commandProcessor, IConsole console, bool useHelpBuilder)
-            {
-                _commandProcessor = commandProcessor;
-                _console = console;
-                _useHelpBuilder = useHelpBuilder;
-            }
-
-            void IHelpBuilder.Write(ICommand command)
-            {
-                bool useHelpBuilder = _useHelpBuilder;
-                if (_commandProcessor._commandHandlers.TryGetValue(command.Name, out CommandHandler handler))
-                {
-                    if (handler.InvokeHelp(_commandProcessor._parser, LocalConsole.ToServices(_console))) {
-                        return;
-                    }
-                    useHelpBuilder = true;
-                }
-                if (useHelpBuilder)
-                {
-                    var helpBuilder = new HelpBuilder(_console, maxWidth: Console.WindowWidth);
-                    helpBuilder.Write(command);
-                }
-            }
-        }
-
-        /// <summary>
-        /// This class does two things: wraps the IConsoleService and provides the IConsole interface and 
-        /// pipes through the System.CommandLine parsing allowing per command invocation data (service 
-        /// provider and raw command line) to be passed through.
-        /// </summary>
-        class LocalConsole : IConsole
-        {
-            public static IServiceProvider ToServices(IConsole console) => ((LocalConsole)console).Services;
-
-            public readonly IServiceProvider Services;
-
-            private readonly IConsoleService _console;
-
-            public LocalConsole(IServiceProvider services)
-            {
-                Services = services;
-                _console = services.GetService<IConsoleService>();
-                Debug.Assert(_console != null);
-                Out = new StandardStreamWriter((text) => _console.Write(text));
-                Error = new StandardStreamWriter((text) => _console.WriteError(text));
-            }
-
-            #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.Diagnostics.Repl/Command/HelpCommand.cs b/src/Microsoft.Diagnostics.Repl/Command/HelpCommand.cs
deleted file mode 100644 (file)
index 309a786..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.DebugServices;
-using System;
-using System.CommandLine;
-using System.CommandLine.Help;
-
-namespace Microsoft.Diagnostics.Repl
-{
-    [Command(Name = "help", Help = "Display help for a command.", Platform = CommandPlatform.Global)]
-    public class HelpCommand : CommandBase
-    {
-        [Argument(Help = "Command to find help.")]
-        public string Command { get; set; }
-
-        private readonly CommandProcessor _commandProcessor;
-        private readonly IServiceProvider _services;
-
-        public HelpCommand(CommandProcessor commandProcessor, IServiceProvider services)
-        {
-            _commandProcessor = commandProcessor;
-            _services = services;
-        }
-
-        public override void Invoke()
-        {
-            if (!_commandProcessor.DisplayHelp(Command, _services))
-            {
-                throw new NotSupportedException($"Help for {Command} not found");
-            }
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostics.Repl/Console/CharToLineConverter.cs b/src/Microsoft.Diagnostics.Repl/Console/CharToLineConverter.cs
deleted file mode 100644 (file)
index 323b27b..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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 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();
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostics.Repl/Console/ConsoleProvider.cs b/src/Microsoft.Diagnostics.Repl/Console/ConsoleProvider.cs
deleted file mode 100644 (file)
index 2272d2e..0000000
+++ /dev/null
@@ -1,556 +0,0 @@
-// 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.DebugServices;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading;
-
-namespace Microsoft.Diagnostics.Repl
-{
-    public sealed class ConsoleProvider : IConsoleService
-    {
-        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_outputRedirected;
-        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_shutdown = false;
-
-            m_consoleConverter = new CharToLineConverter(text => {
-                NewOutput(text);
-            });
-
-            m_warningConverter = new CharToLineConverter(text => {
-                NewOutput(text, warningColor);
-            });
-
-            m_errorConverter = new CharToLineConverter(text => {
-                NewOutput(text, errorColor);
-            });
-
-            // 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 void Start(Action<string, CancellationToken> dispatchCommand)
-        {
-            m_lastCommandLine = null;
-            m_interactiveConsole = !Console.IsInputRedirected;
-            m_outputRedirected = Console.IsOutputRedirected;
-            RefreshLine();
-
-            // The special prompts for the test runner are built into this
-            // console provider when the output has been redirected.
-            if (!m_interactiveConsole) {
-                WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
-            }
-
-            // Start keyboard processing
-            while (!m_shutdown) {
-                if (m_interactiveConsole)
-                {
-                    ConsoleKeyInfo keyInfo = Console.ReadKey(true);
-                    ProcessKeyInfo(keyInfo, dispatchCommand);
-                }
-                else
-                {
-                    // The input has been redirected (i.e. testing or in script)
-                    string line = Console.ReadLine();
-                    if (string.IsNullOrEmpty(line)) {
-                        continue;
-                    }
-                    bool result = Dispatch(line, dispatchCommand);
-                    if (!m_shutdown)
-                    {
-                        if (result) {
-                            WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
-                        }
-                        else {
-                            WriteLine(OutputType.Normal, "<END_COMMAND_ERROR>");
-                        }
-                    }
-                }
-            }
-        }
-
-        public bool Shutdown { get { return m_shutdown; } }
-
-        /// <summary>
-        /// Stop input processing/dispatching
-        /// </summary>
-        public void Stop()
-        {
-            ClearLine();
-            m_shutdown = true;
-            // Delete the last command (usually q or exit) that caused Stop() to be
-            // called so the history doesn't fill up with exit commands.
-            if (m_selectedHistory > 0) {
-                m_history.RemoveAt(--m_selectedHistory);
-            }
-            Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
-        }
-
-        /// <summary>
-        /// Returns the current command history for serialization.
-        /// </summary>
-        public IEnumerable<string> GetCommandHistory()
-        {
-            return m_history.Select((sb) => sb.ToString());
-        }
-
-        /// <summary>
-        /// Adds the command history.
-        /// </summary>
-        /// <param name="commandHistory">command history strings to add</param>
-        public void AddCommandHistory(IEnumerable<string> commandHistory)
-        {
-            m_history.AddRange(commandHistory.Select((s) => new StringBuilder(s)));
-            m_selectedHistory = m_history.Count;
-        }
-
-        /// <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(string format, params object[] parameters)
-        {
-            WriteLine(OutputType.Normal, format, parameters);
-        }
-
-        /// <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, parameters) + Environment.NewLine);
-        }
-
-        /// <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);
-
-            if (!m_outputRedirected) {
-                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);
-
-            if (!m_outputRedirected) {
-                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 void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action<string, CancellationToken> 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;
-
-                    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 bool Dispatch(string newCommand, Action<string, CancellationToken> dispatchCommand)
-        {
-            bool result = true;
-            CommandStarting();
-            m_interruptExecutingCommand = new CancellationTokenSource();
-            ((IConsoleService)this).CancellationToken = m_interruptExecutingCommand.Token;
-            try
-            {
-                newCommand = newCommand.Trim();
-                if (string.IsNullOrEmpty(newCommand) && m_lastCommandLine != null) {
-                    newCommand = m_lastCommandLine;
-                }
-                try
-                {
-                    WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand);
-                    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);
-                    Trace.TraceError(ex.ToString());
-                    m_lastCommandLine = null;
-                    result = false;
-                }
-            }
-            finally
-            {
-                m_interruptExecutingCommand = null;
-                CommandFinished();
-            }
-            return result;
-        }
-
-        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 IConsoleService
-
-        void IConsoleService.Write(string text) => WriteOutput(OutputType.Normal, text);
-
-        void IConsoleService.WriteWarning(string text) => WriteOutput(OutputType.Warning, text);
-
-        void IConsoleService.WriteError(string text) => WriteOutput(OutputType.Error, text);
-
-        CancellationToken IConsoleService.CancellationToken { get; set; }
-
-        #endregion
-    }
-}
diff --git a/src/Microsoft.Diagnostics.Repl/Console/ExitCommand.cs b/src/Microsoft.Diagnostics.Repl/Console/ExitCommand.cs
deleted file mode 100644 (file)
index c7678c3..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// 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.DebugServices;
-using System;
-
-namespace Microsoft.Diagnostics.Repl
-{
-    [Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exit interactive mode.", Platform = CommandPlatform.Global)]
-    public class ExitCommand : CommandBase
-    {
-        private readonly Action _exit;
-
-        public ExitCommand(Action exit)
-        {
-            _exit = exit;
-        }
-
-        public override void Invoke()
-        {
-            _exit();
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostics.Repl/Console/OutputType.cs b/src/Microsoft.Diagnostics.Repl/Console/OutputType.cs
deleted file mode 100644 (file)
index 53fb714..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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.Repl
-{
-    /// <summary>
-    /// The type of output. 
-    /// </summary>
-    public enum OutputType
-    {
-        Normal      = 1,
-        Error       = 2,
-        Warning     = 3,
-    }
-}
diff --git a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs
new file mode 100644 (file)
index 0000000..c2f96af
--- /dev/null
@@ -0,0 +1,556 @@
+// 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.DebugServices;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace Microsoft.Diagnostics.Repl
+{
+    public sealed class ConsoleService : IConsoleService
+    {
+        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_outputRedirected;
+        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 ConsoleService(ConsoleColor errorColor = ConsoleColor.Red, ConsoleColor warningColor = ConsoleColor.Yellow)
+        {
+            m_history = new List<StringBuilder>();
+            m_activeLine = new StringBuilder();
+            m_shutdown = false;
+
+            m_consoleConverter = new CharToLineConverter(text => {
+                NewOutput(text);
+            });
+
+            m_warningConverter = new CharToLineConverter(text => {
+                NewOutput(text, warningColor);
+            });
+
+            m_errorConverter = new CharToLineConverter(text => {
+                NewOutput(text, errorColor);
+            });
+
+            // 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 void Start(Action<string, CancellationToken> dispatchCommand)
+        {
+            m_lastCommandLine = null;
+            m_interactiveConsole = !Console.IsInputRedirected;
+            m_outputRedirected = Console.IsOutputRedirected;
+            RefreshLine();
+
+            // The special prompts for the test runner are built into this
+            // console provider when the output has been redirected.
+            if (!m_interactiveConsole) {
+                WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
+            }
+
+            // Start keyboard processing
+            while (!m_shutdown) {
+                if (m_interactiveConsole)
+                {
+                    ConsoleKeyInfo keyInfo = Console.ReadKey(true);
+                    ProcessKeyInfo(keyInfo, dispatchCommand);
+                }
+                else
+                {
+                    // The input has been redirected (i.e. testing or in script)
+                    string line = Console.ReadLine();
+                    if (string.IsNullOrEmpty(line)) {
+                        continue;
+                    }
+                    bool result = Dispatch(line, dispatchCommand);
+                    if (!m_shutdown)
+                    {
+                        if (result) {
+                            WriteLine(OutputType.Normal, "<END_COMMAND_OUTPUT>");
+                        }
+                        else {
+                            WriteLine(OutputType.Normal, "<END_COMMAND_ERROR>");
+                        }
+                    }
+                }
+            }
+        }
+
+        public bool Shutdown { get { return m_shutdown; } }
+
+        /// <summary>
+        /// Stop input processing/dispatching
+        /// </summary>
+        public void Stop()
+        {
+            ClearLine();
+            m_shutdown = true;
+            // Delete the last command (usually q or exit) that caused Stop() to be
+            // called so the history doesn't fill up with exit commands.
+            if (m_selectedHistory > 0) {
+                m_history.RemoveAt(--m_selectedHistory);
+            }
+            Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnCtrlBreakKeyPress);
+        }
+
+        /// <summary>
+        /// Returns the current command history for serialization.
+        /// </summary>
+        public IEnumerable<string> GetCommandHistory()
+        {
+            return m_history.Select((sb) => sb.ToString());
+        }
+
+        /// <summary>
+        /// Adds the command history.
+        /// </summary>
+        /// <param name="commandHistory">command history strings to add</param>
+        public void AddCommandHistory(IEnumerable<string> commandHistory)
+        {
+            m_history.AddRange(commandHistory.Select((s) => new StringBuilder(s)));
+            m_selectedHistory = m_history.Count;
+        }
+
+        /// <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(string format, params object[] parameters)
+        {
+            WriteLine(OutputType.Normal, format, parameters);
+        }
+
+        /// <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, parameters) + Environment.NewLine);
+        }
+
+        /// <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);
+
+            if (!m_outputRedirected) {
+                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);
+
+            if (!m_outputRedirected) {
+                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 void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action<string, CancellationToken> 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;
+
+                    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 bool Dispatch(string newCommand, Action<string, CancellationToken> dispatchCommand)
+        {
+            bool result = true;
+            CommandStarting();
+            m_interruptExecutingCommand = new CancellationTokenSource();
+            ((IConsoleService)this).CancellationToken = m_interruptExecutingCommand.Token;
+            try
+            {
+                newCommand = newCommand.Trim();
+                if (string.IsNullOrEmpty(newCommand) && m_lastCommandLine != null) {
+                    newCommand = m_lastCommandLine;
+                }
+                try
+                {
+                    WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand);
+                    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);
+                    Trace.TraceError(ex.ToString());
+                    m_lastCommandLine = null;
+                    result = false;
+                }
+            }
+            finally
+            {
+                m_interruptExecutingCommand = null;
+                CommandFinished();
+            }
+            return result;
+        }
+
+        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 IConsoleService
+
+        void IConsoleService.Write(string text) => WriteOutput(OutputType.Normal, text);
+
+        void IConsoleService.WriteWarning(string text) => WriteOutput(OutputType.Warning, text);
+
+        void IConsoleService.WriteError(string text) => WriteOutput(OutputType.Error, text);
+
+        CancellationToken IConsoleService.CancellationToken { get; set; }
+
+        #endregion
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Repl/ExitCommand.cs b/src/Microsoft.Diagnostics.Repl/ExitCommand.cs
new file mode 100644 (file)
index 0000000..c7678c3
--- /dev/null
@@ -0,0 +1,25 @@
+// 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.DebugServices;
+using System;
+
+namespace Microsoft.Diagnostics.Repl
+{
+    [Command(Name = "exit", Aliases = new string[] { "quit", "q" }, Help = "Exit interactive mode.", Platform = CommandPlatform.Global)]
+    public class ExitCommand : CommandBase
+    {
+        private readonly Action _exit;
+
+        public ExitCommand(Action exit)
+        {
+            _exit = exit;
+        }
+
+        public override void Invoke()
+        {
+            _exit();
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Repl/HelpCommand.cs b/src/Microsoft.Diagnostics.Repl/HelpCommand.cs
new file mode 100644 (file)
index 0000000..cbf3699
--- /dev/null
@@ -0,0 +1,33 @@
+// 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.DebugServices;
+using System;
+
+namespace Microsoft.Diagnostics.Repl
+{
+    [Command(Name = "help", Help = "Display help for a command.", Platform = CommandPlatform.Global)]
+    public class HelpCommand : CommandBase
+    {
+        [Argument(Help = "Command to find help.")]
+        public string Command { get; set; }
+
+        private readonly ICommandService _commandService;
+        private readonly IServiceProvider _services;
+
+        public HelpCommand(ICommandService commandService, IServiceProvider services)
+        {
+            _commandService = commandService;
+            _services = services;
+        }
+
+        public override void Invoke()
+        {
+            if (!_commandService.DisplayHelp(Command, _services))
+            {
+                throw new NotSupportedException($"Help for {Command} not found");
+            }
+        }
+    }
+}
index 4523fa90d92c7530c6771d0f73b7f3f7408f3d47..9016c741969ee0928e8c0bd2b58283cd83344414 100644 (file)
     <IsShippingPackage>false</IsShippingPackage>
   </PropertyGroup>
   
-  <ItemGroup>
-    <PackageReference Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
-  </ItemGroup>
-  
   <ItemGroup>
     <ProjectReference Include="..\Microsoft.Diagnostics.DebugServices\Microsoft.Diagnostics.DebugServices.csproj" />
   </ItemGroup>
diff --git a/src/Microsoft.Diagnostics.Repl/OutputType.cs b/src/Microsoft.Diagnostics.Repl/OutputType.cs
new file mode 100644 (file)
index 0000000..53fb714
--- /dev/null
@@ -0,0 +1,16 @@
+// 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.Repl
+{
+    /// <summary>
+    /// The type of output. 
+    /// </summary>
+    public enum OutputType
+    {
+        Normal      = 1,
+        Error       = 2,
+        Warning     = 3,
+    }
+}
index 29572e36ba9941064879eb1742f95dd66620043e..aac0399996a0186521931811816c5425b9239e01 100644 (file)
@@ -5,7 +5,6 @@
 using Microsoft.Diagnostics.DebugServices;
 using Microsoft.Diagnostics.DebugServices.Implementation;
 using Microsoft.Diagnostics.ExtensionCommands;
-using Microsoft.Diagnostics.Repl;
 using Microsoft.Diagnostics.Runtime;
 using Microsoft.Diagnostics.Runtime.Utilities;
 using SOS.Hosting;
@@ -38,7 +37,7 @@ namespace SOS.Extensions
         internal DebuggerServices DebuggerServices { get; private set; }
 
         private readonly ServiceProvider _serviceProvider;
-        private readonly CommandProcessor _commandProcessor;
+        private readonly CommandService _commandService;
         private readonly SymbolService _symbolService;
         private readonly HostWrapper _hostWrapper;
         private ContextServiceFromDebuggerServices _contextService;
@@ -99,12 +98,12 @@ namespace SOS.Extensions
         {
             _serviceProvider = new ServiceProvider();
             _symbolService = new SymbolService(this);
-            _commandProcessor = new CommandProcessor(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ">!ext" : null);
-            _commandProcessor.AddCommands(new Assembly[] { typeof(HostServices).Assembly });
-            _commandProcessor.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly });
+            _commandService = new CommandService(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ">!ext" : null);
+            _commandService.AddCommands(new Assembly[] { typeof(HostServices).Assembly });
+            _commandService.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly });
 
             _serviceProvider.AddService<IHost>(this);
-            _serviceProvider.AddService<ICommandService>(_commandProcessor);
+            _serviceProvider.AddService<ICommandService>(_commandService);
             _serviceProvider.AddService<ISymbolService>(_symbolService);
 
             _hostWrapper = new HostWrapper(this, () => _targetWrapper);
@@ -220,7 +219,7 @@ namespace SOS.Extensions
                 });
 
                 // Add each extension command to the native debugger
-                foreach ((string name, string help, IEnumerable<string> aliases) in _commandProcessor.Commands)
+                foreach ((string name, string help, IEnumerable<string> aliases) in _commandService.Commands)
                 {
                     hr = DebuggerServices.AddCommand(name, help, aliases);
                     if (hr != HResult.S_OK)
@@ -325,7 +324,7 @@ namespace SOS.Extensions
             }
             try
             {
-                return _commandProcessor.Execute(commandLine, _contextService.Services);
+                return _commandService.Execute(commandLine, _contextService.Services);
             }
             catch (Exception ex)
             {
@@ -340,7 +339,7 @@ namespace SOS.Extensions
         {
             try
             {
-                if (!_commandProcessor.DisplayHelp(command, _contextService.Services))
+                if (!_commandService.DisplayHelp(command, _contextService.Services))
                 {
                     return HResult.E_INVALIDARG;
                 }
index b03fef3106a270679413c140193ad0806969d893..fc0ab84f3f2964d20dc20a79e8907705cd60686d 100644 (file)
@@ -20,7 +20,6 @@
   <ItemGroup>
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.DebugServices\Microsoft.Diagnostics.DebugServices.csproj" />
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.DebugServices.Implementation\Microsoft.Diagnostics.DebugServices.Implementation.csproj" />
-    <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Repl\Microsoft.Diagnostics.Repl.csproj" />
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.ExtensionCommands\Microsoft.Diagnostics.ExtensionCommands.csproj" />
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\SOS.Hosting\SOS.Hosting.csproj" />
   </ItemGroup>
index c088ed697dc6ec9695a6ec5002cd51da9ac80327..0a1e7e8ae1972472ee176203e7d7437c09595e84 100644 (file)
@@ -2,30 +2,29 @@
 // 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.ExtensionCommands;
 using Microsoft.Diagnostics.DebugServices;
 using Microsoft.Diagnostics.DebugServices.Implementation;
+using Microsoft.Diagnostics.ExtensionCommands;
 using Microsoft.Diagnostics.Repl;
 using Microsoft.Diagnostics.Runtime;
 using SOS.Hosting;
 using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 using System.Runtime.InteropServices;
+using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security;
-using System.Diagnostics;
 
 namespace Microsoft.Diagnostics.Tools.Dump
 {
     public class Analyzer : IHost
     {
         private readonly ServiceProvider _serviceProvider;
-        private readonly ConsoleProvider _consoleProvider;
-        private readonly CommandProcessor _commandProcessor;
+        private readonly ConsoleService _consoleProvider;
+        private readonly CommandService _commandService;
         private readonly SymbolService _symbolService;
         private readonly ContextService _contextService;
         private int _targetIdFactory;
@@ -36,14 +35,14 @@ namespace Microsoft.Diagnostics.Tools.Dump
             LoggingCommand.Initialize();
 
             _serviceProvider = new ServiceProvider();
-            _consoleProvider = new ConsoleProvider();
-            _commandProcessor = new CommandProcessor();
+            _consoleProvider = new ConsoleService();
+            _commandService = new CommandService();
             _symbolService = new SymbolService(this);
             _contextService = new ContextService(this);
 
             _serviceProvider.AddService<IHost>(this);
             _serviceProvider.AddService<IConsoleService>(_consoleProvider);
-            _serviceProvider.AddService<ICommandService>(_commandProcessor);
+            _serviceProvider.AddService<ICommandService>(_commandService);
             _serviceProvider.AddService<ISymbolService>(_symbolService);
             _serviceProvider.AddService<IContextService>(_contextService);
             _serviceProvider.AddServiceFactory<SOSLibrary>(() => SOSLibrary.Create(this));
@@ -53,11 +52,11 @@ namespace Microsoft.Diagnostics.Tools.Dump
                 return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null;
             });
 
-            _commandProcessor.AddCommands(new Assembly[] { typeof(Analyzer).Assembly });
-            _commandProcessor.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly });
-            _commandProcessor.AddCommands(new Assembly[] { typeof(SOSHost).Assembly });
-            _commandProcessor.AddCommands(typeof(HelpCommand), (services) => new HelpCommand(_commandProcessor, services));
-            _commandProcessor.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleProvider.Stop));
+            _commandService.AddCommands(new Assembly[] { typeof(Analyzer).Assembly });
+            _commandService.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly });
+            _commandService.AddCommands(new Assembly[] { typeof(SOSHost).Assembly });
+            _commandService.AddCommands(typeof(HelpCommand), (services) => new HelpCommand(_commandService, services));
+            _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleProvider.Stop));
         }
 
         public Task<int> Analyze(FileInfo dump_path, string[] command)
@@ -112,7 +111,7 @@ namespace Microsoft.Diagnostics.Tools.Dump
                 {
                     foreach (string cmd in command)
                     {
-                        _commandProcessor.Execute(cmd, _contextService.Services);
+                        _commandService.Execute(cmd, _contextService.Services);
                         if (_consoleProvider.Shutdown) {
                             break;
                         }
@@ -125,7 +124,7 @@ namespace Microsoft.Diagnostics.Tools.Dump
                     _consoleProvider.WriteLine("Type 'quit' or 'exit' to exit the session.");
 
                     _consoleProvider.Start((string commandLine, CancellationToken cancellation) => {
-                        _commandProcessor.Execute(commandLine, _contextService.Services);
+                        _commandService.Execute(commandLine, _contextService.Services);
                     });
                 }
             }
@@ -228,7 +227,7 @@ namespace Microsoft.Diagnostics.Tools.Dump
             }
             if (assembly is not null)
             {
-                _commandProcessor.AddCommands(assembly);
+                _commandService.AddCommands(assembly);
                 _consoleProvider.WriteLine($"Extension loaded {extensionPath}");
             }
         }