From 4fa131b75b4fcec1f91f383a52ea2b42167bc0b3 Mon Sep 17 00:00:00 2001 From: Zhentar Date: Fri, 29 Mar 2019 09:11:51 -0500 Subject: [PATCH 1/5] Add DbgMemory RGB output format --- DbgProvider/public/ColorString.cs | 9 ++++ .../public/Commands/ReadDbgMemoryCommand.cs | 3 ++ DbgProvider/public/Debugger/DbgMemory.cs | 54 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/DbgProvider/public/ColorString.cs b/DbgProvider/public/ColorString.cs index 8f06233..a3fef73 100644 --- a/DbgProvider/public/ColorString.cs +++ b/DbgProvider/public/ColorString.cs @@ -314,6 +314,15 @@ namespace MS.Dbg return this; } + public ColorString AppendFgRgb( byte r, byte g, byte b, ColorString other ) + { + if( null == other ) + return this; + _CheckReadOnly( true ); + m_elements.Add( new ContentElement( $"\x01b[38;2;{r};{g};{b}m" )); + return Append( other ); + } + public ColorString AppendLine( ColorString other ) { Append( other ); diff --git a/DbgProvider/public/Commands/ReadDbgMemoryCommand.cs b/DbgProvider/public/Commands/ReadDbgMemoryCommand.cs index 0257652..4947b25 100644 --- a/DbgProvider/public/Commands/ReadDbgMemoryCommand.cs +++ b/DbgProvider/public/Commands/ReadDbgMemoryCommand.cs @@ -171,9 +171,12 @@ namespace MS.Dbg.Commands case DbgMemoryDisplayFormat.Words: case DbgMemoryDisplayFormat.WordsWithAscii: return 2; + case DbgMemoryDisplayFormat.RGB: + return 1; // Technically should be 3 but want to allow flexibility for line padding case DbgMemoryDisplayFormat.DWords: case DbgMemoryDisplayFormat.DWordsWithAscii: case DbgMemoryDisplayFormat.DWordsWithBits: + case DbgMemoryDisplayFormat.RGBA: return 4; case DbgMemoryDisplayFormat.QWords: case DbgMemoryDisplayFormat.QWordsWithAscii: diff --git a/DbgProvider/public/Debugger/DbgMemory.cs b/DbgProvider/public/Debugger/DbgMemory.cs index 4fa0561..de5f343 100644 --- a/DbgProvider/public/Debugger/DbgMemory.cs +++ b/DbgProvider/public/Debugger/DbgMemory.cs @@ -37,7 +37,9 @@ namespace MS.Dbg Pointers, PointersWithSymbols, PointersWithAscii, - PointersWithSymbolsAndAscii + PointersWithSymbolsAndAscii, + RGB, + RGBA } public class DbgMemory : ISupportColor, ICloneable @@ -275,9 +277,13 @@ namespace MS.Dbg case DbgMemoryDisplayFormat.Words: case DbgMemoryDisplayFormat.WordsWithAscii: return Words[ index ]; + case DbgMemoryDisplayFormat.RGB: + var rgbIdx = index * 3; + return (uint)(Bytes[ rgbIdx ] + (Bytes[ rgbIdx + 1 ] << 8) + (Bytes[ rgbIdx + 2 ] << 16)); case DbgMemoryDisplayFormat.DWords: case DbgMemoryDisplayFormat.DWordsWithAscii: case DbgMemoryDisplayFormat.DWordsWithBits: + case DbgMemoryDisplayFormat.RGBA: return DWords[index]; case DbgMemoryDisplayFormat.QWords: case DbgMemoryDisplayFormat.QWordsWithAscii: @@ -310,9 +316,12 @@ namespace MS.Dbg case DbgMemoryDisplayFormat.Words: case DbgMemoryDisplayFormat.WordsWithAscii: return Words.Count; + case DbgMemoryDisplayFormat.RGB: + return Bytes.Count / 3; case DbgMemoryDisplayFormat.DWords: case DbgMemoryDisplayFormat.DWordsWithAscii: case DbgMemoryDisplayFormat.DWordsWithBits: + case DbgMemoryDisplayFormat.RGBA: return DWords.Count; case DbgMemoryDisplayFormat.QWords: case DbgMemoryDisplayFormat.QWordsWithAscii: @@ -433,6 +442,10 @@ namespace MS.Dbg return _FormatBlocks( 4, 9, 1, AddtlInfo.Symbols | AddtlInfo.Ascii ); else return _FormatBlocks( 8, 18, 1, AddtlInfo.Symbols | AddtlInfo.Ascii ); + case DbgMemoryDisplayFormat.RGB: + return _FormatRGB( numColumns, false ); + case DbgMemoryDisplayFormat.RGBA: + return _FormatRGB( numColumns, true ); default: throw new NotImplementedException(); } @@ -611,6 +624,45 @@ namespace MS.Dbg return cs.MakeReadOnly(); } // end _FormatBlocks() + private static readonly string[] AlphaChars = { "░", "▒", "▓", "█" }; + + private ColorString _FormatRGB( uint numColumns, bool withAlpha ) + { + if( 0 == numColumns ) + numColumns = 64; + + ColorString cs = new ColorString(); + + var bytesPerCharacter = withAlpha ? 4 : 3; + int bytesPerRow = (int)numColumns * bytesPerCharacter + 3 & ~(3); //round up + + for( int rowStart = 0; rowStart + bytesPerCharacter < Bytes.Count; rowStart += bytesPerRow ) + { + if(rowStart != 0) + { + cs.AppendLine(); + } + cs.Append( DbgProvider.FormatAddress( StartAddress + (uint) rowStart, m_is32Bit, true, true ) ).Append( " " ); + + var rowLen = Math.Min( Bytes.Count - rowStart, bytesPerRow ); + + for( int colOffset = 0; colOffset + bytesPerCharacter < rowLen; colOffset+= bytesPerCharacter ) + { + byte b = Bytes[ rowStart + colOffset + 0 ]; + byte g = Bytes[ rowStart + colOffset + 1 ]; + byte r = Bytes[ rowStart + colOffset + 2 ]; + string ch = "█"; + if( withAlpha ) + { + ch = AlphaChars[ Bytes[ rowStart + colOffset + 3 ] >> 6 ]; + } + cs.AppendFgRgb( r, g, b, ch ); + } + } + + return cs.MakeReadOnly(); + + } private ColorString _FormatCharsOnly( uint numColumns, uint bytesPerChar, Func< char, char > toDisplay ) { From fa7abbf5f45073df843e1a98ed3a8ae2ac17e396 Mon Sep 17 00:00:00 2001 From: Zhentar Date: Sun, 16 Jun 2019 22:35:06 -0500 Subject: [PATCH 2/5] Pull in upstream commit PowerShell/PowerShell@2174dd8 (with a lot of hacking to make it fit) --- DbgProvider/DbgProvider.csproj | 13 +-- DbgProvider/app.config | 11 +++ DbgProvider/packages.config | 8 +- DbgShell/App.config | 10 ++- DbgShell/ColorHostUserInterface.cs | 39 ++++++--- DbgShell/ConsoleControl.AnsiColorWriter.cs | 29 ++++--- DbgShell/ConsoleControl.cs | 94 +++++++++++----------- DbgShell/ConsoleTextWriter.cs | 6 +- DbgShell/DbgShell.csproj | 9 +++ DbgShell/packages.config | 3 + 10 files changed, 133 insertions(+), 89 deletions(-) create mode 100644 DbgProvider/app.config diff --git a/DbgProvider/DbgProvider.csproj b/DbgProvider/DbgProvider.csproj index 60ed64d..69f010c 100644 --- a/DbgProvider/DbgProvider.csproj +++ b/DbgProvider/DbgProvider.csproj @@ -73,8 +73,8 @@ - - ..\packages\System.Buffers.4.4.0\lib\netstandard1.1\System.Buffers.dll + + ..\packages\System.Buffers.4.5.0\lib\netstandard1.1\System.Buffers.dll @@ -82,11 +82,11 @@ False ..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll - - ..\packages\System.Memory.4.5.1\lib\netstandard1.1\System.Memory.dll + + ..\packages\System.Memory.4.5.3\lib\netstandard1.1\System.Memory.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll @@ -292,6 +292,7 @@ + PreserveNewest diff --git a/DbgProvider/app.config b/DbgProvider/app.config new file mode 100644 index 0000000..f6e7dca --- /dev/null +++ b/DbgProvider/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/DbgProvider/packages.config b/DbgProvider/packages.config index 237c208..9934a56 100644 --- a/DbgProvider/packages.config +++ b/DbgProvider/packages.config @@ -1,9 +1,9 @@  - - - + + + - + \ No newline at end of file diff --git a/DbgShell/App.config b/DbgShell/App.config index 3511aa7..aa38394 100644 --- a/DbgShell/App.config +++ b/DbgShell/App.config @@ -1,11 +1,15 @@ - + - + - + + + + + diff --git a/DbgShell/ColorHostUserInterface.cs b/DbgShell/ColorHostUserInterface.cs index 25c4f2b..9c7cb6d 100644 --- a/DbgShell/ColorHostUserInterface.cs +++ b/DbgShell/ColorHostUserInterface.cs @@ -12,6 +12,7 @@ using System.Text; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Host; +using System.Runtime.CompilerServices; using System.Security; using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle; @@ -639,7 +640,26 @@ namespace MS.DbgShell #region WriteToConsole + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteToConsole(char c, bool transcribeResult) + { + ReadOnlySpan value = stackalloc char[1] {c}; + WriteToConsole(value, transcribeResult); + } + internal void WriteToConsole(string value, bool transcribeResult) + { + WriteToConsole(value.AsSpan(), transcribeResult, newLine: false); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteToConsole(ReadOnlySpan value, bool transcribeResult) + { + WriteToConsole(value, transcribeResult, newLine: false); + } + + internal void WriteToConsole(ReadOnlySpan value, bool transcribeResult, bool newLine) { ConsoleHandle handle = ConsoleControl.GetActiveScreenBufferHandle(); @@ -662,13 +682,13 @@ namespace MS.DbgShell // This is atomic, so we don't lock here... - ConsoleControl.WriteConsole(handle, value); + ConsoleControl.WriteConsole(handle, value, newLine); - if (transcribeResult) - { - PostWrite(value); - } - else + //if (transcribeResult) + //{ + // PostWrite(value); + //} + //else { PostWrite(); } @@ -697,12 +717,10 @@ namespace MS.DbgShell private void WriteLineToConsole(string text) { - WriteToConsole(text, true); - WriteToConsole(Crlf, true); + WriteToConsole(text.AsSpan(), transcribeResult: true, newLine: true ); } - private void WriteLineToConsole() { WriteToConsole(Crlf, true); @@ -832,8 +850,7 @@ namespace MS.DbgShell lock (_instanceLock) { - this.Write(value); - this.Write(Crlf); + this.WriteToConsole(value.AsSpan(), transcribeResult: true, newLine: true); } } diff --git a/DbgShell/ConsoleControl.AnsiColorWriter.cs b/DbgShell/ConsoleControl.AnsiColorWriter.cs index 0520924..2c08af1 100644 --- a/DbgShell/ConsoleControl.AnsiColorWriter.cs +++ b/DbgShell/ConsoleControl.AnsiColorWriter.cs @@ -148,32 +148,35 @@ internal static partial class ConsoleControl } // end constructor - public void Write( ConsoleHandle handle, string s ) + public void Write( ConsoleHandle handle, ReadOnlySpan< char > s, bool appendNewline ) { - int startIndex = 0; - if( m_state.IsParsing ) { // Need to continue. - startIndex = _HandleControlSequence( s, -1 ); + int startIndex = _HandleControlSequence( s, -1 ); + s = s.Slice( startIndex ); } - int escIndex = s.IndexOf( CSI, startIndex ); + int escIndex = s.IndexOf( CSI ); while( escIndex >= 0 ) { //Tool.WL( "escIndex: {0}, startIndex: {1}", escIndex, startIndex ); - string chunk = s.Substring( startIndex, escIndex - startIndex ); - //Console.Write( chunk ); + var chunk = s.Slice( 0, escIndex ); ConsoleControl._RealWriteConsole( handle, chunk ); - startIndex = _HandleControlSequence( s, escIndex ); - escIndex = s.IndexOf( CSI, startIndex ); + int startIndex = _HandleControlSequence( s, escIndex ); + s = s.Slice( startIndex ); + escIndex = s.IndexOf( CSI ); } - if( !(startIndex >= s.Length) ) + if( !s.IsEmpty ) { - //Console.Write( s.Substring( startIndex ) ); - ConsoleControl._RealWriteConsole( handle, s.Substring( startIndex ) ); + ConsoleControl._RealWriteConsole( handle, s ); + } + + if( appendNewline ) + { + ConsoleControl._RealWriteConsole( handle, ColorHostUserInterface.Crlf.AsSpan() ); } } // end Write() @@ -183,7 +186,7 @@ internal static partial class ConsoleControl /// could be past the end of the string, or could be the start of another /// control sequence). /// - private int _HandleControlSequence( string s, int escIndex ) + private int _HandleControlSequence( ReadOnlySpan< char > s, int escIndex ) { m_state.Begin(); // we may actually be continuing... diff --git a/DbgShell/ConsoleControl.cs b/DbgShell/ConsoleControl.cs index 8bf10ed..a5e3a2f 100644 --- a/DbgShell/ConsoleControl.cs +++ b/DbgShell/ConsoleControl.cs @@ -2727,95 +2727,89 @@ namespace MS.DbgShell return LucidaSupportedCodePages.Contains(currentLocaleCodePage); } -#endregion + #endregion /// - /// /// Wrap Win32 WriteConsole - /// /// /// - /// /// handle for the console where the string is written - /// /// /// - /// /// string that is written - /// + /// + /// + /// New line is written. /// /// - /// /// if the Win32's WriteConsole fails - /// /// - - internal static void WriteConsole(ConsoleHandle consoleHandle, string output) + internal static void WriteConsole( ConsoleHandle consoleHandle, ReadOnlySpan< char > output, bool newLine = false ) { - Util.Assert(!consoleHandle.IsInvalid, "ConsoleHandle is not valid"); - Util.Assert(!consoleHandle.IsClosed, "ConsoleHandle is closed"); + Util.Assert( !consoleHandle.IsInvalid, "ConsoleHandle is not valid" ); + Util.Assert( !consoleHandle.IsClosed, "ConsoleHandle is closed" ); - if (String.IsNullOrEmpty(output)) - return; - - // Native WriteConsole doesn't support output buffer longer than 64K. - // We need to chop the output string if it is too long. - - int cursor = 0; // This records the chopping position in output string - const int maxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character. - - while (cursor < output.Length) + if( output.IsEmpty ) { - string outBuffer; - - if (cursor + maxBufferSize < output.Length) + if( newLine ) { - outBuffer = output.Substring(cursor, maxBufferSize); - cursor += maxBufferSize; + _RealWriteConsole( consoleHandle, Environment.NewLine.AsSpan() ); + } + return; + } + + // [danthom] Debugging note: put a breakpoint here to catch output as it + // is going out. + + // If a newline gets injected between strings where a color control + // sequence is broken across them, things blow up terribly. + int cursor = 0; // This records the chopping position in output string + const int MaxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character. + while( cursor < output.Length ) + { + ReadOnlySpan< char > outBuffer; + + if( cursor + MaxBufferSize < output.Length ) + { + outBuffer = output.Slice( cursor, MaxBufferSize ); + cursor += MaxBufferSize; } else { - outBuffer = output.Substring(cursor); + outBuffer = output.Slice( cursor ); cursor = output.Length; } - //_RealWriteConsole( consoleHandle, outBuffer ); - - // [danthom] Debugging note: put a breakpoint here to catch output as it - // is going out. - - // If a newline gets injected between strings where a color control - // sequence is broken across them, things blow up terribly. - if( 0 == Util.Strcmp_OI( ColorHostUserInterface.Crlf, outBuffer ) ) + if( ColorHostUserInterface.Crlf.AsSpan().SequenceEqual( outBuffer ) ) { _RealWriteConsole( consoleHandle, outBuffer ); } else { - sm_colorWriter.Write( consoleHandle, outBuffer ); + sm_colorWriter.Write( consoleHandle, outBuffer, newLine ); } } - } // end WriteConsole() + + }// end WriteConsole() // A problem with doing this at such an incredibly low level is that if there is any // kind of problem with it, and then we try to WriteErrorLine... and hilarity ensues. // TODO: a possible mitigation is if we detect an invalid control code or somesuch, // reset the colors and parse state. - private static AnsiColorWriter sm_colorWriter = new AnsiColorWriter(); + private static readonly AnsiColorWriter sm_colorWriter = new AnsiColorWriter(); - private static void _RealWriteConsole( ConsoleHandle handle, string s ) + private static void _RealWriteConsole( ConsoleHandle handle, ReadOnlySpan< char > s ) { - DWORD charsWritten; - bool itWorked = NativeMethods.WriteConsole( handle.DangerousGetHandle(), - s, + bool itWorked = NativeMethods.WriteConsole( handle, + in MemoryMarshal.GetReference( s ), (DWORD) s.Length, - out charsWritten, + out uint _, IntPtr.Zero ); if( !itWorked ) { int err = Marshal.GetLastWin32Error(); - HostException e = CreateHostException(err, "WriteConsole", + HostException e = CreateHostException( err, "WriteConsole", ErrorCategory.WriteError, "The Win32 internal error \"{0}\" 0x{1:X} occurred while writing to the console output buffer at the current cursor position. Contact Microsoft Customer Support Services." ); //ConsoleControlStrings.WriteConsoleExceptionTemplate); throw e; } @@ -3496,11 +3490,13 @@ namespace MS.DbgShell out DWORD numberOfCharsWritten ); - [DllImport("KERNEL32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + // [zhent] unlike PowerShell Core, we use a ref char instead of char*. This doesn't require compiling with /unsafe, + // allowing us pretend we are doing something more safe (we aren't). + [DllImport( "KERNEL32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode )] internal static extern bool WriteConsole ( - NakedWin32Handle consoleOutput, - string buffer, + SafeHandle consoleOutput, + in char buffer, DWORD numberOfCharsToWrite, out DWORD numberOfCharsWritten, IntPtr reserved diff --git a/DbgShell/ConsoleTextWriter.cs b/DbgShell/ConsoleTextWriter.cs index da6c914..1f80785 100644 --- a/DbgShell/ConsoleTextWriter.cs +++ b/DbgShell/ConsoleTextWriter.cs @@ -66,7 +66,7 @@ namespace MS.DbgShell void WriteLine(string value) { - this.Write(value + ColorHostUserInterface.Crlf); + _ui.WriteToConsole( value.AsSpan(), true, newLine: true ); } @@ -84,7 +84,7 @@ namespace MS.DbgShell void Write(Char c) { - this.Write(new String(c, 1)); + _ui.WriteToConsole( c, true ); } @@ -93,7 +93,7 @@ namespace MS.DbgShell void Write(Char[] a) { - this.Write(new String(a)); + _ui.WriteToConsole( a.AsSpan(), true ); } diff --git a/DbgShell/DbgShell.csproj b/DbgShell/DbgShell.csproj index 2f719f8..fdf4ef7 100644 --- a/DbgShell/DbgShell.csproj +++ b/DbgShell/DbgShell.csproj @@ -69,12 +69,21 @@ ..\..\..\..\..\..\..\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\Microsoft.PowerShell.ConsoleHost.dll + + ..\packages\System.Buffers.4.5.0\lib\netstandard1.1\System.Buffers.dll + False ..\..\..\..\..\..\..\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll + + ..\packages\System.Memory.4.5.3\lib\netstandard1.1\System.Memory.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll + diff --git a/DbgShell/packages.config b/DbgShell/packages.config index 5c91e72..731e855 100644 --- a/DbgShell/packages.config +++ b/DbgShell/packages.config @@ -1,4 +1,7 @@  + + + \ No newline at end of file From c55cfbea9494256ce128a866b42815bb1abb5411 Mon Sep 17 00:00:00 2001 From: Zhentar Date: Tue, 18 Jun 2019 22:18:18 -0500 Subject: [PATCH 3/5] Make extended SGR RGB a first class, push/pop supported escape sequence. Overhaul AnsiColorWriter to use VT sequences instead of Console API when VT support is present (huge output performance improvement from reduced context switches & associated overhead) --- DbgProvider/DbgProvider.csproj | 1 - DbgProvider/internal/AnsiColorWriter.cs | 412 ----------- DbgProvider/public/ColorString.cs | 15 +- DbgShell/ColorHostUserInterface.cs | 10 +- DbgShell/ConsoleControl.AnsiColorWriter.cs | 805 +++++++++++++-------- DbgShell/ConsoleControl.cs | 52 +- DbgShell/DbgShell.csproj | 2 + 7 files changed, 534 insertions(+), 763 deletions(-) delete mode 100644 DbgProvider/internal/AnsiColorWriter.cs diff --git a/DbgProvider/DbgProvider.csproj b/DbgProvider/DbgProvider.csproj index 69f010c..a0c9804 100644 --- a/DbgProvider/DbgProvider.csproj +++ b/DbgProvider/DbgProvider.csproj @@ -90,7 +90,6 @@ - diff --git a/DbgProvider/internal/AnsiColorWriter.cs b/DbgProvider/internal/AnsiColorWriter.cs deleted file mode 100644 index 13327b5..0000000 --- a/DbgProvider/internal/AnsiColorWriter.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MS.Dbg -{ - // This class interprets ANSI escape sequences to alter the color of console output. Only - // a limited set of control sequences are supported, namely a subset of SGR (Select - // Graphics Rendition) commands. - // - // See: - // - // http://bjh21.me.uk/all-escapes/all-escapes.txt - // http://en.wikipedia.org/wiki/ISO/IEC_6429 - // http://en.wikipedia.org/wiki/ISO_6429 - // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - // - internal class AnsiColorWriter : TextWriter - { - private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[') - - private ConsoleColor DefaultForeground; - private ConsoleColor DefaultBackground; - private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot; - private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_hashCommands; - private ControlSequenceParseState m_state; - - - // We support control sequences being broken across calls to Write methods. This - // struct keeps track of state in between calls. - private struct ControlSequenceParseState - { - private int m_accum; - private bool m_isParsingParam; - private List< int > m_list; - - // True if we are in the middle of parsing a sequence. Useful for when sequences - // are broken across multiple calls. - public bool IsParsing - { - get - { - return null != CurrentCommands; - } - } - - private IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot; - - public ControlSequenceParseState( IReadOnlyDictionary< char, Func< Action< List< int > > > > rootCommands ) - { - m_commandTreeRoot = rootCommands; - CurrentCommands = null; - m_accum = 0; - m_isParsingParam = false; - m_list = null; - } - - public IReadOnlyDictionary< char, Func< Action< List< int > > > > CurrentCommands; - - // This can tolerate multiple Begin calls without destroying state. - public void Begin() - { - if( !IsParsing ) - { - CurrentCommands = m_commandTreeRoot; - m_list = new List< int >(); - m_isParsingParam = false; - } - } - - public void AccumParamDigit( int digit ) - { - m_accum *= 10; - m_accum += digit; - m_isParsingParam = true; - } - - public void EnterParam() - { - Util.Assert( m_isParsingParam ); - m_list.Add( m_accum ); - m_isParsingParam = false; - m_accum = 0; - } - - public List< int > FinishParsingParams() - { - if( m_isParsingParam ) - EnterParam(); - - CurrentCommands = null; - List< int > list = m_list; - m_list = null; - return list; - } - - /// - /// To recover from bad input. - /// - public void Reset() - { - CurrentCommands = null; - m_isParsingParam = false; - m_list = null; - m_accum = 0; - } - } // end class ControlSequenceParseState - - - public AnsiColorWriter() - { - DefaultForeground = Console.ForegroundColor; - DefaultBackground = Console.BackgroundColor; - - m_commandTreeRoot = new Dictionary< char, Func< Action< List< int > > > >() - { - { 'm', () => _SelectGraphicsRendition }, - { '#', _ProcessHashCommand }, - }; - - m_hashCommands = new Dictionary< char, Func< Action< List< int > > > >() - { - // TROUBLE: The current definition of XTPUSHSGR and XTPOPSGR use curly - // brackets, which turns out to conflict badly with C# string formatting. - // For example, this: - // - // - // $csFmt = (New-ColorString).AppendPushFg( 'Cyan' ).Append( 'this should all be cyan: {0}' ) - // [string]::format( $csFmt.ToString( $true ), 'blah' ) - // - // will blow up. - // - // For now, I'm going to switch to some other characters while we see if - // we get can something worked out with xterm. - //{ '{', () => _PushSgr }, - //{ '}', () => _PopSgr }, - { 'p', () => _PushSgr }, - { 'q', () => _PopSgr }, - }; - - m_state = new ControlSequenceParseState( m_commandTreeRoot ); - } // end constructor - - - public override Encoding Encoding { get { return Console.Out.Encoding; } } - - - public override void Write( char[] charBuffer, int index, int count) - { - Write( new String( charBuffer, index, count ) ); - } - - public override void Write( char c ) - { - Write( c.ToString() ); - } - - public override void Write( string s ) - { - int startIndex = 0; - - if( m_state.IsParsing ) - { - // Need to continue. - startIndex = _HandleControlSequence( s, -1 ); - } - - int escIndex = s.IndexOf( CSI, startIndex ); - - while( escIndex >= 0 ) - { - //Tool.WL( "escIndex: {0}, startIndex: {1}", escIndex, startIndex ); - string chunk = s.Substring( startIndex, escIndex - startIndex ); - Console.Write( chunk ); - startIndex = _HandleControlSequence( s, escIndex ); - escIndex = s.IndexOf( CSI, startIndex ); - } - - if( !(startIndex >= s.Length) ) - { - Console.Write( s.Substring( startIndex ) ); - } - } // end Write() - - - /// - /// Returns the index of the first character past the control sequence (which - /// could be past the end of the string, or could be the start of another - /// control sequence). - /// - private int _HandleControlSequence( string s, int escIndex ) - { - m_state.Begin(); // we may actually be continuing... - - char c; - int curIndex = escIndex; - while( ++curIndex < s.Length ) - { - c = s[ curIndex ]; - if( (c >= '0') && (c <= '9') ) - { - m_state.AccumParamDigit( ((int) c) - 0x30 ); - continue; - } - else if( ';' == c ) - { - m_state.EnterParam(); - continue; - } - else if( ((c >= '@') && (c <= '~')) || (c == '#') ) - { - if( _FindCommand( c, out Action< List< int > > command ) ) - { - command( m_state.FinishParsingParams() ); - return curIndex + 1; - } - } - else - { - // You're supposed to be able to have anonther character, like a space - // (0x20) before the command code character, but I'm not going to do that. - - // TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the - // output stream, and that string had control sequences. And say then somebody tried to process - // that string in a pipeline, for instance tried to find some property on it, that didn't - // exist, causing an error to be written, where the "target object" is the string... and stuff - // maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this? - // Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm. - m_state.Reset(); - throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) ); - } - } - // Finished parsing the whole string--the control sequence must be broken across - // strings. - return curIndex; - } // end _HandleControlSequence() - - - /// - /// Returns true if it successfully found a command to execute; false if we - /// need to read more characters (some commands are expressed with multiple - /// characters). - /// - private bool _FindCommand( char commandChar, out Action< List< int > > command ) - { - Func< Action< List< int > > > commandSearcher; - if( !m_state.CurrentCommands.TryGetValue( commandChar, out commandSearcher ) ) - { - throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) ); - } - - command = commandSearcher(); - - return command != null; - } // end _FindCommand() - - - // Info sources: - // http://bjh21.me.uk/all-escapes/all-escapes.txt - // http://en.wikipedia.org/wiki/ISO/IEC_6429 - // http://en.wikipedia.org/wiki/ISO_6429 - // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - private static ConsoleColor[] AnsiNormalColorMap = - { - // Foreground Background - ConsoleColor.Black, // 30 40 - ConsoleColor.DarkRed, // 31 41 - ConsoleColor.DarkGreen, // 32 42 - ConsoleColor.DarkYellow, // 33 43 - ConsoleColor.DarkBlue, // 34 44 - ConsoleColor.DarkMagenta, // 35 45 - ConsoleColor.DarkCyan, // 36 46 - ConsoleColor.Gray // 37 47 - }; - - private static ConsoleColor[] AnsiBrightColorMap = - { // Foreground Background - ConsoleColor.DarkGray, // 90 100 - ConsoleColor.Red, // 91 101 - ConsoleColor.Green, // 92 102 - ConsoleColor.Yellow, // 93 103 - ConsoleColor.Blue, // 94 104 - ConsoleColor.Magenta, // 95 105 - ConsoleColor.Cyan, // 96 106 - ConsoleColor.White // 97 107 - }; - - // In addition to the color codes, I've added support for two additional - // (non-standard) codes: - // - // 56: Push fg/bg color pair - // 57: Pop fg/bg color pair - // - // However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead. - // - - private void _SelectGraphicsRendition( List< int > args ) - { - if( 0 == args.Count ) - args.Add( 0 ); // no args counts as a reset - - foreach( int arg in args ) - { - _ProcessSgrCode( arg ); - } - } // end _SelectGraphicsRendition() - - - private void _PushSgr( List< int > args ) - { - if( (args != null) && (args.Count != 0) ) - { - throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." ); - } - - m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) ); - } // end _PushSgr() - - - private void _PopSgr( List< int > args ) - { - if( (args != null) && (args.Count != 0) ) - { - throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." ); - } - - ColorPair cp = m_colorStack.Pop(); - Console.ForegroundColor = cp.Foreground; - Console.BackgroundColor = cp.Background; - } // end _PopSgr() - - - private Action< List< int > > _ProcessHashCommand() - { - m_state.CurrentCommands = m_hashCommands; - - return null; - } // end _SelectGraphicsRendition() - - - private class ColorPair - { - public ConsoleColor Foreground { get; private set; } - public ConsoleColor Background { get; private set; } - public ColorPair( ConsoleColor foreground, ConsoleColor background ) - { - Foreground = foreground; - Background = background; - } - } - - private Stack< ColorPair > m_colorStack = new Stack< ColorPair >(); - - private void _ProcessSgrCode( int code ) - { - if( 0 == code ) - { - Console.ForegroundColor = DefaultForeground; - Console.BackgroundColor = DefaultBackground; - } - else if( (code <= 37) && (code >= 30) ) - { - Console.ForegroundColor = AnsiNormalColorMap[ (code - 30) ]; - } - else if( (code <= 47) && (code >= 40) ) - { - Console.BackgroundColor = AnsiNormalColorMap[ (code - 40) ]; - } - else if( (code <= 97) && (code >= 90) ) - { - Console.ForegroundColor = AnsiBrightColorMap[ (code - 90) ]; - } - else if( (code <= 107) && (code >= 100) ) - { - Console.BackgroundColor = AnsiBrightColorMap[ (code - 100) ]; - } - else if( 56 == code ) // NON-STANDARD (I made this one up) - { - Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); - m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) ); - } - else if( 57 == code ) // NON-STANDARD (I made this one up) - { - Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); - ColorPair cp = m_colorStack.Pop(); - Console.ForegroundColor = cp.Foreground; - Console.BackgroundColor = cp.Background; - } - else - { - throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) ); - } - } // end _ProcessSgrCode() - - public static void Test() - { - AnsiColorWriter colorWriter = new AnsiColorWriter(); - colorWriter.Write( "\u009b91mThis should be red.\u009bm This should not.\r\n" ); - colorWriter.Write( "\u009b91;41mThis should be red on red.\u009bm This should not.\r\n" ); - colorWriter.Write( "\u009b91;100mThis should be red on gray.\u009b" ); - colorWriter.Write( "m This should not.\r\n" ); - - colorWriter.Write( "\u009b" ); - colorWriter.Write( "9" ); - colorWriter.Write( "1;10" ); - colorWriter.Write( "6mThis should be red on cyan.\u009b" ); - colorWriter.Write( "m This should \u009bm\u009bmnot.\r\n" ); - } // end Test - - } // end class AnsiColorWriter -} diff --git a/DbgProvider/public/ColorString.cs b/DbgProvider/public/ColorString.cs index a3fef73..ea52fa7 100644 --- a/DbgProvider/public/ColorString.cs +++ b/DbgProvider/public/ColorString.cs @@ -313,13 +313,22 @@ namespace MS.Dbg m_apparentLength += other.m_apparentLength; return this; } - - public ColorString AppendFgRgb( byte r, byte g, byte b, ColorString other ) + + public ColorString AppendFgRgb( byte r, byte g, byte b, ColorString other = null) { + _CheckReadOnly( true ); + m_elements.Add( new SgrControlSequence( new[] { 38, 2, r, g, b } ) ); if( null == other ) return this; + return Append( other ); + } + + public ColorString AppendBgRgb( byte r, byte g, byte b, ColorString other = null ) + { _CheckReadOnly( true ); - m_elements.Add( new ContentElement( $"\x01b[38;2;{r};{g};{b}m" )); + m_elements.Add( new SgrControlSequence( new[] { 48, 2, r, g, b } ) ); + if( null == other ) + return this; return Append( other ); } diff --git a/DbgShell/ColorHostUserInterface.cs b/DbgShell/ColorHostUserInterface.cs index 9c7cb6d..3eb37c1 100644 --- a/DbgShell/ColorHostUserInterface.cs +++ b/DbgShell/ColorHostUserInterface.cs @@ -60,15 +60,7 @@ namespace MS.DbgShell // Turn on virtual terminal if possible. // This might throw - not sure how exactly (no console), but if it does, we shouldn't fail to start. - var handle = ConsoleControl.GetActiveScreenBufferHandle(); - var m = ConsoleControl.GetMode(handle); - if (ConsoleControl.NativeMethods.SetConsoleMode(handle.DangerousGetHandle(), (uint)(m | ConsoleControl.ConsoleModes.VirtualTerminal))) - { - // We only know if vt100 is supported if the previous call actually set the new flag, older - // systems ignore the setting. - m = ConsoleControl.GetMode(handle); - this.SupportsVirtualTerminal = (m & ConsoleControl.ConsoleModes.VirtualTerminal) != 0; - } + SupportsVirtualTerminal = ConsoleControl.CheckVirtualTerminalSupported(); } catch { diff --git a/DbgShell/ConsoleControl.AnsiColorWriter.cs b/DbgShell/ConsoleControl.AnsiColorWriter.cs index 2c08af1..629dc50 100644 --- a/DbgShell/ConsoleControl.AnsiColorWriter.cs +++ b/DbgShell/ConsoleControl.AnsiColorWriter.cs @@ -7,124 +7,136 @@ using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle; namespace MS.DbgShell { -// (unindented a level to simplify the diff with other similar copies of this code) -internal static partial class ConsoleControl -{ - // This class interprets ANSI escape sequences to alter the color of console output. Only - // a limited set of control sequences are supported, namely a subset of SGR (Select - // Graphics Rendition) commands. - // - // See: - // - // http://bjh21.me.uk/all-escapes/all-escapes.txt - // http://en.wikipedia.org/wiki/ISO/IEC_6429 - // http://en.wikipedia.org/wiki/ISO_6429 - // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - // - internal class AnsiColorWriter + internal static partial class ConsoleControl { - private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[') - - private ConsoleColor DefaultForeground; - private ConsoleColor DefaultBackground; - private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot; - private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_hashCommands; - private ControlSequenceParseState m_state; - - - // We support control sequences being broken across calls to Write methods. This - // struct keeps track of state in between calls. - private struct ControlSequenceParseState + // This class interprets ANSI escape sequences to alter the color of console output. Only + // a limited set of control sequences are supported, namely a subset of SGR (Select + // Graphics Rendition) commands. + // + // See: + // + // http://bjh21.me.uk/all-escapes/all-escapes.txt + // http://en.wikipedia.org/wiki/ISO/IEC_6429 + // http://en.wikipedia.org/wiki/ISO_6429 + // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + // + internal class AnsiColorWriter { - private int m_accum; - private bool m_isParsingParam; - private List< int > m_list; + private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[') + private const string CSI_str = "\x9b"; //String version for printing (was sticking the char in a stackalloc span, but hit https://github.com/dotnet/roslyn/issues/35874 ) - // True if we are in the middle of parsing a sequence. Useful for when sequences - // are broken across multiple calls. - public bool IsParsing + private readonly ConsoleColor DefaultForeground; + private readonly ConsoleColor DefaultBackground; + private ControlSequenceParseState m_state; + + + public bool VirtualTerminalSupported { get; set; } + + // We support control sequences being broken across calls to Write methods. This + // struct keeps track of state in between calls. + private struct ControlSequenceParseState { - get + private int m_accum; + private bool m_isParsingParam; + private List< int > m_list; + + // True if we are in the middle of parsing a sequence. Useful for when sequences + // are broken across multiple calls. + public bool IsParsing { - return null != CurrentCommands; + get + { + return null != CurrentCommandHandler; + } } - } - private IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot; + private readonly Func< char, bool > m_commandTreeRootDelegate; - public ControlSequenceParseState( IReadOnlyDictionary< char, Func< Action< List< int > > > > rootCommands ) - { - m_commandTreeRoot = rootCommands; - CurrentCommands = null; - m_accum = 0; - m_isParsingParam = false; - m_list = null; - } - - public IReadOnlyDictionary< char, Func< Action< List< int > > > > CurrentCommands; - - // This can tolerate multiple Begin calls without destroying state. - public void Begin() - { - if( !IsParsing ) + public ControlSequenceParseState( Func< char, bool > rootCommands ) { - CurrentCommands = m_commandTreeRoot; - m_list = new List< int >(); + m_commandTreeRootDelegate = rootCommands; + CurrentCommandHandler = null; + m_accum = 0; m_isParsingParam = false; + m_list = null; + } + + public Func< char, bool > CurrentCommandHandler; + + // This can tolerate multiple Begin calls without destroying state. + public void Begin() + { + if( !IsParsing ) + { + CurrentCommandHandler = m_commandTreeRootDelegate; + m_list = new List< int >(); + m_isParsingParam = false; + } + } + + public void AccumParamDigit( int digit ) + { + m_accum *= 10; + m_accum += digit; + m_isParsingParam = true; + } + + public void EnterParam() + { + Util.Assert( m_isParsingParam ); + m_list.Add( m_accum ); + m_isParsingParam = false; + m_accum = 0; + } + + public List< int > FinishParsingParams() + { + if( m_isParsingParam ) + EnterParam(); + + CurrentCommandHandler = null; + List< int > list = m_list; + m_list = null; + return list; + } + + /// + /// To recover from bad input. + /// + public void Reset() + { + CurrentCommandHandler = null; + m_isParsingParam = false; + m_list = null; + m_accum = 0; + } + } // end class ControlSequenceParseState + + + public AnsiColorWriter() + { + DefaultForeground = ConsoleControl.ForegroundColor; + DefaultBackground = ConsoleControl.BackgroundColor; + + m_state = new ControlSequenceParseState( CommandTreeRootStateHandler ); + } // end constructor + + private bool CommandTreeRootStateHandler( char commandChar ) + { + switch( commandChar ) + { + case 'm': + _SelectGraphicsRendition( m_state.FinishParsingParams() ); + return true; + case '#': + m_state.CurrentCommandHandler = HashCommandStateHandler; + return false; + default: + throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) ); } } - public void AccumParamDigit( int digit ) - { - m_accum *= 10; - m_accum += digit; - m_isParsingParam = true; - } - - public void EnterParam() - { - Util.Assert( m_isParsingParam ); - m_list.Add( m_accum ); - m_isParsingParam = false; - m_accum = 0; - } - - public List< int > FinishParsingParams() - { - if( m_isParsingParam ) - EnterParam(); - - CurrentCommands = null; - List< int > list = m_list; - m_list = null; - return list; - } - - /// - /// To recover from bad input. - /// - public void Reset() - { - CurrentCommands = null; - m_isParsingParam = false; - m_list = null; - m_accum = 0; - } - } // end class ControlSequenceParseState - - - public AnsiColorWriter() - { - DefaultForeground = ConsoleControl.ForegroundColor; - DefaultBackground = ConsoleControl.BackgroundColor; - - m_commandTreeRoot = new Dictionary< char, Func< Action< List< int > > > >() - { - { 'm', () => _SelectGraphicsRendition }, - { '#', _ProcessHashCommand }, - }; - - m_hashCommands = new Dictionary< char, Func< Action< List< int > > > >() + private bool HashCommandStateHandler( char commandChar ) { // TROUBLE: The current definition of XTPUSHSGR and XTPOPSGR use curly // brackets, which turns out to conflict badly with C# string formatting. @@ -140,256 +152,425 @@ internal static partial class ConsoleControl // we get can something worked out with xterm. //{ '{', () => _PushSgr }, //{ '}', () => _PopSgr }, - { 'p', () => _PushSgr }, - { 'q', () => _PopSgr }, - }; - - m_state = new ControlSequenceParseState( m_commandTreeRoot ); - } // end constructor - - - public void Write( ConsoleHandle handle, ReadOnlySpan< char > s, bool appendNewline ) - { - if( m_state.IsParsing ) - { - // Need to continue. - int startIndex = _HandleControlSequence( s, -1 ); - s = s.Slice( startIndex ); - } - - int escIndex = s.IndexOf( CSI ); - - while( escIndex >= 0 ) - { - //Tool.WL( "escIndex: {0}, startIndex: {1}", escIndex, startIndex ); - var chunk = s.Slice( 0, escIndex ); - ConsoleControl._RealWriteConsole( handle, chunk ); - int startIndex = _HandleControlSequence( s, escIndex ); - s = s.Slice( startIndex ); - escIndex = s.IndexOf( CSI ); - } - - if( !s.IsEmpty ) - { - ConsoleControl._RealWriteConsole( handle, s ); - } - - if( appendNewline ) - { - ConsoleControl._RealWriteConsole( handle, ColorHostUserInterface.Crlf.AsSpan() ); - } - } // end Write() - - - /// - /// Returns the index of the first character past the control sequence (which - /// could be past the end of the string, or could be the start of another - /// control sequence). - /// - private int _HandleControlSequence( ReadOnlySpan< char > s, int escIndex ) - { - m_state.Begin(); // we may actually be continuing... - - char c; - int curIndex = escIndex; - while( ++curIndex < s.Length ) - { - c = s[ curIndex ]; - if( (c >= '0') && (c <= '9') ) + switch( commandChar ) { - m_state.AccumParamDigit( ((int) c) - 0x30 ); - continue; + case 'p': + _PushSgr( m_state.FinishParsingParams() ); + return true; + case 'q': + _PopSgr( m_state.FinishParsingParams() ); + return true; + default: + throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) ); } - else if( ';' == c ) + } + + + private char[] m_outputCharBuffer = new char[ 16384 ]; + private int m_outputBufferPos = 0; + + public void Write( ConsoleHandle handle, ReadOnlySpan< char > s, bool appendNewline ) + { + m_outputBufferPos = 0; + + if( m_state.IsParsing ) { - m_state.EnterParam(); - continue; + // Need to continue. + int startIndex = _HandleControlSequence( s, -1 ); + s = s.Slice( startIndex ); } - else if( ((c >= '@') && (c <= '~')) || (c == '#') ) + + int escIndex = s.IndexOf( CSI ); + + while( escIndex >= 0 ) { - if( _FindCommand( c, out Action< List< int > > command ) ) - { - command( m_state.FinishParsingParams() ); - return curIndex + 1; - } + var chunk = s.Slice( 0, escIndex ); + WriteConsoleWrapper( handle, chunk ); + int startIndex = _HandleControlSequence( s, escIndex ); + s = s.Slice( startIndex ); + escIndex = s.IndexOf( CSI ); + } + + if( !s.IsEmpty ) + { + WriteConsoleWrapper( handle, s ); + } + + if( appendNewline ) + { + WriteConsoleWrapper( handle, ColorHostUserInterface.Crlf.AsSpan() ); + } + FinalizeWrite( handle ); + } // end Write() + + private void WriteConsoleWrapper( ConsoleHandle handle, ReadOnlySpan value ) + { + if( VirtualTerminalSupported ) + { + AppendToOutputBuffer( value ); } else { - // You're supposed to be able to have anonther character, like a space - // (0x20) before the command code character, but I'm not going to do that. + // [zhent] the PowerShell source claims that WriteConsole breaks down past 64KB. But it also seems to think `char` is/can be 4 bytes, so I'm skeptical. + // In my tests on Windows 10, it can handle at least up to 4GB (with enough patience), so I'll leave the chunking for versions that aren't recent enough to support VT + while( value.Length > 16383 ) + { + _RealWriteConsole( handle, value.Slice( 0, 16383 ) ); + value = value.Slice( 16383 ); + } - // TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the - // output stream, and that string had control sequences. And say then somebody tried to process - // that string in a pipeline, for instance tried to find some property on it, that didn't - // exist, causing an error to be written, where the "target object" is the string... and stuff - // maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this? - // Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm. - m_state.Reset(); - throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) ); + _RealWriteConsole( handle, value ); } } - // Finished parsing the whole string--the control sequence must be broken across - // strings. - return curIndex; - } // end _HandleControlSequence() - - /// - /// Returns true if it successfully found a command to execute; false if we - /// need to read more characters (some commands are expressed with multiple - /// characters). - /// - private bool _FindCommand( char commandChar, out Action< List< int > > command ) - { - Func< Action< List< int > > > commandSearcher; - if( !m_state.CurrentCommands.TryGetValue( commandChar, out commandSearcher ) ) + private void AppendToOutputBuffer( ReadOnlySpan value ) { - throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) ); + var destSpan = m_outputCharBuffer.AsSpan( m_outputBufferPos ); + while( value.Length > destSpan.Length ) + { + var newBuff = new char[ m_outputCharBuffer.Length * 2 ]; + m_outputCharBuffer.AsSpan( 0, m_outputBufferPos ).CopyTo( newBuff.AsSpan() ); + m_outputCharBuffer = newBuff; + destSpan = m_outputCharBuffer.AsSpan( m_outputBufferPos ); + } + value.CopyTo( destSpan ); + m_outputBufferPos += value.Length; } - command = commandSearcher(); - - return command != null; - } // end _FindCommand() + // Commented out due to https://github.com/dotnet/roslyn/issues/35874 + //private void AppendToOutputBuffer( char value ) + //{ + // Span< char > tempBuff = stackalloc char[ 1 ]; + // tempBuff[ 0 ] = value; + // AppendToOutputBuffer( tempBuff ); + //} - // Info sources: - // http://bjh21.me.uk/all-escapes/all-escapes.txt - // http://en.wikipedia.org/wiki/ISO/IEC_6429 - // http://en.wikipedia.org/wiki/ISO_6429 - // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - private static ConsoleColor[] AnsiNormalColorMap = - { - // Foreground Background - ConsoleColor.Black, // 30 40 - ConsoleColor.DarkRed, // 31 41 - ConsoleColor.DarkGreen, // 32 42 - ConsoleColor.DarkYellow, // 33 43 - ConsoleColor.DarkBlue, // 34 44 - ConsoleColor.DarkMagenta, // 35 45 - ConsoleColor.DarkCyan, // 36 46 - ConsoleColor.Gray // 37 47 - }; - - private static ConsoleColor[] AnsiBrightColorMap = - { // Foreground Background - ConsoleColor.DarkGray, // 90 100 - ConsoleColor.Red, // 91 101 - ConsoleColor.Green, // 92 102 - ConsoleColor.Yellow, // 93 103 - ConsoleColor.Blue, // 94 104 - ConsoleColor.Magenta, // 95 105 - ConsoleColor.Cyan, // 96 106 - ConsoleColor.White // 97 107 - }; - - // In addition to the color codes, I've added support for two additional - // (non-standard) codes: - // - // 56: Push fg/bg color pair - // 57: Pop fg/bg color pair - // - // However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead. - // - - private void _SelectGraphicsRendition( List< int > args ) - { - if( 0 == args.Count ) - args.Add( 0 ); // no args counts as a reset - - foreach( int arg in args ) + private void FinalizeWrite( ConsoleHandle handle ) { - _ProcessSgrCode( arg ); - } - } // end _SelectGraphicsRendition() - - - private void _PushSgr( List< int > args ) - { - if( (args != null) && (args.Count != 0) ) - { - throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." ); + if( VirtualTerminalSupported ) + { + _RealWriteConsole( handle, m_outputCharBuffer.AsSpan( 0, m_outputBufferPos ) ); + } } - m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) ); - } // end _PushSgr() + + /// + /// Returns the index of the first character past the control sequence (which + /// could be past the end of the string, or could be the start of another + /// control sequence). + /// + private int _HandleControlSequence( ReadOnlySpan< char > s, int escIndex ) + { + m_state.Begin(); // we may actually be continuing... + + int curIndex = escIndex; + while( ++curIndex < s.Length ) + { + char c = s[ curIndex ]; + if( (c >= '0') && (c <= '9') ) + { + m_state.AccumParamDigit( ((int) c) - 0x30 ); + continue; + } + else if( ';' == c ) + { + m_state.EnterParam(); + continue; + } + else if( ((c >= '@') && (c <= '~')) || (c == '#') ) + { + if( m_state.CurrentCommandHandler( c ) ) + { + return curIndex + 1; + } + } + else + { + // You're supposed to be able to have another character, like a space + // (0x20) before the command code character, but I'm not going to do that. + + // TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the + // output stream, and that string had control sequences. And say then somebody tried to process + // that string in a pipeline, for instance tried to find some property on it, that didn't + // exist, causing an error to be written, where the "target object" is the string... and stuff + // maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this? + // Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm. + m_state.Reset(); + throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) ); + } + } + // Finished parsing the whole string--the control sequence must be broken across + // strings. + return curIndex; + } // end _HandleControlSequence() - private void _PopSgr( List< int > args ) - { - if( (args != null) && (args.Count != 0) ) + // Info sources: + // http://bjh21.me.uk/all-escapes/all-escapes.txt + // http://en.wikipedia.org/wiki/ISO/IEC_6429 + // http://en.wikipedia.org/wiki/ISO_6429 + // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + private static ConsoleColor[] AnsiNormalColorMap = { - throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." ); - } + // Foreground Background + ConsoleColor.Black, // 30 40 + ConsoleColor.DarkRed, // 31 41 + ConsoleColor.DarkGreen, // 32 42 + ConsoleColor.DarkYellow, // 33 43 + ConsoleColor.DarkBlue, // 34 44 + ConsoleColor.DarkMagenta, // 35 45 + ConsoleColor.DarkCyan, // 36 46 + ConsoleColor.Gray // 37 47 + }; - ColorPair cp = m_colorStack.Pop(); - Console.ForegroundColor = cp.Foreground; - Console.BackgroundColor = cp.Background; - } // end _PopSgr() + private static ConsoleColor[] AnsiBrightColorMap = + { // Foreground Background + ConsoleColor.DarkGray, // 90 100 + ConsoleColor.Red, // 91 101 + ConsoleColor.Green, // 92 102 + ConsoleColor.Yellow, // 93 103 + ConsoleColor.Blue, // 94 104 + ConsoleColor.Magenta, // 95 105 + ConsoleColor.Cyan, // 96 106 + ConsoleColor.White // 97 107 + }; + + // In addition to the color codes, I've added support for two additional + // (non-standard) codes: + // + // 56: Push fg/bg color pair + // 57: Pop fg/bg color pair + // + // However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead. + // + + private void _SelectGraphicsRendition( List< int > args ) + { + if( 0 == args.Count ) + args.Add( 0 ); // no args counts as a reset + + int index = 0; + while( index < args.Count ) + { + _ProcessSgrCode( args, ref index ); + } + } // end _SelectGraphicsRendition() - private Action< List< int > > _ProcessHashCommand() - { - m_state.CurrentCommands = m_hashCommands; + private void _PushSgr( List< int > args ) + { + if( (args != null) && (args.Count != 0) ) + { + throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." ); + } - return null; - } // end _SelectGraphicsRendition() + m_colorCodeStack.Push( new ColorCodePair( ForegroundCode, BackgroundCode ) ); + } // end _PushSgr() - private class ColorPair - { - public ConsoleColor Foreground { get; private set; } - public ConsoleColor Background { get; private set; } - public ColorPair( ConsoleColor foreground, ConsoleColor background ) + private void _PopSgr( List< int > args ) { - Foreground = foreground; - Background = background; - } - } + if( (args != null) && (args.Count != 0) ) + { + throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." ); + } - private Stack< ColorPair > m_colorStack = new Stack< ColorPair >(); + var pair = m_colorCodeStack.Pop(); + ForegroundCode = pair.Foreground; + BackgroundCode = pair.Background; + } // end _PopSgr() + + + private struct VtColorCode + { + public VtColorCode( int commandCode ) : this() + { + CommandCode = (byte)commandCode; + } - private void _ProcessSgrCode( int code ) - { - if( 0 == code ) - { - ConsoleControl.ForegroundColor = DefaultForeground; - ConsoleControl.BackgroundColor = DefaultBackground; + public VtColorCode( byte commandCode, byte r, byte g, byte b ) + { + CommandCode = commandCode; + R = r; + G = g; + B = b; + } + + public byte CommandCode { get; } + public byte R { get; } + public byte G { get; } + public byte B { get; } } - else if( (code <= 37) && (code >= 30) ) + + private struct ColorCodePair { - ConsoleControl.ForegroundColor = AnsiNormalColorMap[ (code - 30) ]; + public VtColorCode Foreground { get; } + public VtColorCode Background { get; } + public ColorCodePair( VtColorCode foreground, VtColorCode background ) + { + Foreground = foreground; + Background = background; + } } - else if( (code <= 47) && (code >= 40) ) + + private readonly Stack m_colorCodeStack = new Stack(); + + + private VtColorCode m_activeForegroundCode = new VtColorCode( 39 ); + private VtColorCode m_activeBackgroundCode = new VtColorCode( 49 ); + + private VtColorCode ForegroundCode { - ConsoleControl.BackgroundColor = AnsiNormalColorMap[ (code - 40) ]; + get + { + return m_activeForegroundCode; + } + set + { + m_activeForegroundCode = value; + if( VirtualTerminalSupported ) + { + _WriteCodeToBuffer( value ); + } + else + { + if( value.CommandCode == 0 || value.CommandCode == 39 || value.CommandCode == 38 ) //Treat RGB codes as default since we can't render them correctly + { + ConsoleControl.ForegroundColor = DefaultForeground; + } + else if( (value.CommandCode <= 37) && (value.CommandCode >= 30) ) + { + ConsoleControl.ForegroundColor = AnsiNormalColorMap[ (value.CommandCode - 30) ]; + } + else if( (value.CommandCode <= 97) && (value.CommandCode >= 90) ) + { + ConsoleControl.ForegroundColor = AnsiBrightColorMap[ (value.CommandCode - 90) ]; + } + else + { + throw new InvalidOperationException(); + } + } + } } - else if( (code <= 97) && (code >= 90) ) + + private VtColorCode BackgroundCode { - ConsoleControl.ForegroundColor = AnsiBrightColorMap[ (code - 90) ]; + get + { + return m_activeBackgroundCode; + } + set + { + m_activeBackgroundCode = value; + if( VirtualTerminalSupported ) + { + _WriteCodeToBuffer( value ); + } + else + { + if( value.CommandCode == 0 || value.CommandCode == 49 || value.CommandCode == 48 ) //Treat RGB codes as default since we can't render them correctly + { + ConsoleControl.BackgroundColor = DefaultBackground; + } + else if( (value.CommandCode <= 47) && (value.CommandCode >= 40) ) + { + ConsoleControl.BackgroundColor = AnsiNormalColorMap[ (value.CommandCode - 40) ]; + } + else if( (value.CommandCode <= 107) && (value.CommandCode >= 100) ) + { + ConsoleControl.BackgroundColor = AnsiBrightColorMap[ (value.CommandCode - 100) ]; + } + else + { + throw new InvalidOperationException(); + } + } + } } - else if( (code <= 107) && (code >= 100) ) + + private void _WriteCodeToBuffer( VtColorCode code ) { - ConsoleControl.BackgroundColor = AnsiBrightColorMap[ (code - 100) ]; + AppendToOutputBuffer( CSI_str.AsSpan() ); + AppendToOutputBuffer( sm_byteStrings[ code.CommandCode ].AsSpan() ); + if( code.CommandCode == 38 || code.CommandCode == 48 ) + { + AppendToOutputBuffer( ";2;".AsSpan() ); + AppendToOutputBuffer( sm_byteStrings[ code.R ].AsSpan() ); + AppendToOutputBuffer( ";".AsSpan() ); + AppendToOutputBuffer( sm_byteStrings[ code.G ].AsSpan() ); + AppendToOutputBuffer( ";".AsSpan() ); + AppendToOutputBuffer( sm_byteStrings[ code.B ].AsSpan() ); + } + AppendToOutputBuffer( "m".AsSpan() ); + } - else if( 56 == code ) // NON-STANDARD (I made this one up) + + private void _ProcessSgrCode( List< int > codes, ref int index ) { - Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); - m_colorStack.Push( new ColorPair( ConsoleControl.ForegroundColor, ConsoleControl.BackgroundColor ) ); - } - else if( 57 == code ) // NON-STANDARD (I made this one up) + var code = codes[ index ]; + index++; + if( 0 == code ) + { + //using default foreground/background codes instead of full reset so that the colorcodestack can maintain either one as default independently + ForegroundCode = new VtColorCode( 39 ); + BackgroundCode = new VtColorCode( 49 ); + } + else if( ((code <= 37) && (code >= 30)) || ((code <= 97) && (code >= 90)) || code == 39 ) + { + ForegroundCode = new VtColorCode( code ); + } + else if( ((code <= 47) && (code >= 40)) || ((code <= 107) && (code >= 100)) || code == 49 ) + { + BackgroundCode = new VtColorCode( code ); + } + else if( code == 38 ) + { + ForegroundCode = _ProcessTrueColorSgrCode( code, codes, ref index ); + } + else if( code == 48 ) + { + BackgroundCode = _ProcessTrueColorSgrCode( code, codes, ref index ); + } + else if( 56 == code ) // NON-STANDARD (I made this one up) + { + Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); + m_colorCodeStack.Push( new ColorCodePair( ForegroundCode, BackgroundCode ) ); + } + else if( 57 == code ) // NON-STANDARD (I made this one up) + { + Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); + var pair = m_colorCodeStack.Pop(); + ForegroundCode = pair.Foreground; + BackgroundCode = pair.Background; + } + else + { + throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) ); + } + } // end _ProcessSgrCode() + + private static VtColorCode _ProcessTrueColorSgrCode( int command, List< int > codes, ref int index ) { - Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." ); - ColorPair cp = m_colorStack.Pop(); - ConsoleControl.ForegroundColor = cp.Foreground; - ConsoleControl.BackgroundColor = cp.Background; + if( codes.Count - index < 4 ) { throw new ArgumentException( "Extended SGR sequence does not have enough arguments for RGB colors" ); } + + if( codes[index] != 2 ) { throw new NotSupportedException( $"Extended SGR mode '{codes[ index + 1 ]}' not supported." ); } + index++; + return new VtColorCode( (byte)command, (byte) codes[ index++ ], (byte) codes[ index++ ], (byte) codes[ index++ ] ); } - else - { - throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) ); - } - } // end _ProcessSgrCode() - } // end class AnsiColorWriter -} // class ConsoleControl + + //[zhent] Because the cost of .ToString() is just too much to bear. + private static readonly string[] sm_byteStrings = + { "0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31", + "32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63", + "64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95", + "96","97","98","99","100","101","102","103","104","105","106","107","108","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127", + "128","129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146","147","148","149","150","151","152","153","154","155","156","157","158","159", + "160","161","162","163","164","165","166","167","168","169","170","171","172","173","174","175","176","177","178","179","180","181","182","183","184","185","186","187","188","189","190","191", + "192","193","194","195","196","197","198","199","200","201","202","203","204","205","206","207","208","209","210","211","212","213","214","215","216","217","218","219","220","221","222","223", + "224","225","226","227","228","229","230","231","232","233","234","235","236","237","238","239","240","241","242","243","244","245","246","247","248","249","250","251","252","253","254","255"}; + } // end class AnsiColorWriter + } // class ConsoleControl } // namespace diff --git a/DbgShell/ConsoleControl.cs b/DbgShell/ConsoleControl.cs index a5e3a2f..26ceeb0 100644 --- a/DbgShell/ConsoleControl.cs +++ b/DbgShell/ConsoleControl.cs @@ -747,10 +747,27 @@ namespace MS.DbgShell } } + internal static bool CheckVirtualTerminalSupported() + { + var handle = GetActiveScreenBufferHandle(); + var m = GetMode( handle ); + if( NativeMethods.SetConsoleMode( handle.DangerousGetHandle(), (uint) (m | ConsoleControl.ConsoleModes.VirtualTerminal) ) ) + { + // We only know if vt100 is supported if the previous call actually set the new flag, older + // systems ignore the setting. + m = GetMode( handle ); + if( (m & ConsoleModes.VirtualTerminal) != 0 ) + { + sm_colorWriter.VirtualTerminalSupported = true; + return true; + } + } + return false; + } -#endregion + #endregion -#region Input + #region Input @@ -2758,36 +2775,19 @@ namespace MS.DbgShell return; } + // [danthom] Debugging note: put a breakpoint here to catch output as it // is going out. // If a newline gets injected between strings where a color control // sequence is broken across them, things blow up terribly. - int cursor = 0; // This records the chopping position in output string - const int MaxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character. - while( cursor < output.Length ) + if( ColorHostUserInterface.Crlf.AsSpan().SequenceEqual( output ) ) { - ReadOnlySpan< char > outBuffer; - - if( cursor + MaxBufferSize < output.Length ) - { - outBuffer = output.Slice( cursor, MaxBufferSize ); - cursor += MaxBufferSize; - } - else - { - outBuffer = output.Slice( cursor ); - cursor = output.Length; - } - - if( ColorHostUserInterface.Crlf.AsSpan().SequenceEqual( outBuffer ) ) - { - _RealWriteConsole( consoleHandle, outBuffer ); - } - else - { - sm_colorWriter.Write( consoleHandle, outBuffer, newLine ); - } + _RealWriteConsole( consoleHandle, output ); + } + else + { + sm_colorWriter.Write( consoleHandle, output, newLine ); } }// end WriteConsole() diff --git a/DbgShell/DbgShell.csproj b/DbgShell/DbgShell.csproj index fdf4ef7..12f5aa9 100644 --- a/DbgShell/DbgShell.csproj +++ b/DbgShell/DbgShell.csproj @@ -46,6 +46,7 @@ prompt MinimumRecommendedRules.ruleset false + latest ..\bin\Release\x64\ @@ -57,6 +58,7 @@ prompt MinimumRecommendedRules.ruleset false + latest