| File: Logging\ParallelLogger\ConsoleOutputAligner.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Logging { /// <summary> /// Align output to multiple lines so no logged test is lost due to limited <see cref="Console.BufferWidth"/>. /// During alignment optional prefix/indent is applied. /// </summary> /// <remarks> /// This class is not thread safe. /// </remarks> internal class ConsoleOutputAligner { internal const int ConsoleTabWidth = 8; private readonly int _bufferWidth; private readonly bool _alignMessages; private readonly IStringBuilderProvider _stringBuilderProvider; /// <summary> /// Constructor. /// </summary> /// <param name="bufferWidth">Console buffer width. -1 if unknown/unlimited</param> /// <param name="alignMessages">Whether messages are aligned/wrapped into console buffer width</param> /// <param name="stringBuilderProvider"></param> public ConsoleOutputAligner(int bufferWidth, bool alignMessages, IStringBuilderProvider stringBuilderProvider) { _bufferWidth = bufferWidth; _alignMessages = alignMessages; _stringBuilderProvider = stringBuilderProvider; } /// <summary> /// Based on bufferWidth split message into multiple lines and indent if needed. /// TAB character are interpreted by standard Console logic. /// </summary> /// <param name="message">Input message. May contains tabs and new lines. Both \r\n and \n is supported but replaced into current environment new line.</param> /// <param name="prefixAlreadyWritten">true if message already contains prefix (message context, timestamp, etc...).</param> /// <param name="prefixWidth">Width of the prefix. Every line in result string will be indented by this number of spaces except 1st line with already written prefix.</param> /// <returns>Aligned message ready to be written to Console</returns> /// <remarks> /// For optimization purposes this method uses single <see cref="StringBuilder"/> instance. This makes this method non thread safe. /// Calling side is expected this usage is non-concurrent. This shall not be an issue as it is expected that writing into Console shall be serialized anyway. /// </remarks> public string AlignConsoleOutput(string message, bool prefixAlreadyWritten, int prefixWidth) { int i = 0; int j = message.IndexOfAny(MSBuildConstants.CrLf); // Empiric value of average line length in console output. Used to estimate number of lines in message for StringBuilder capacity. // Wrongly estimated capacity is not a problem as StringBuilder will grow as needed. It is just optimization to avoid multiple reallocations. const int averageLineLength = 40; int estimatedCapacity = message.Length + ((prefixAlreadyWritten ? 0 : prefixWidth) + Environment.NewLine.Length) * (message.Length / averageLineLength + 1); StringBuilder sb = _stringBuilderProvider.Acquire(estimatedCapacity); // The string contains new lines, treat each new line as a different string to format and send to the console while (j >= 0) { AlignAndIndentLineOfMessage(sb, prefixAlreadyWritten, prefixWidth, message, i, j - i); i = j + (message[j] == '\r' && (j + 1) < message.Length && message[j + 1] == '\n' ? 2 : 1); j = message.IndexOfAny(MSBuildConstants.CrLf, i); } // Process rest of message AlignAndIndentLineOfMessage(sb, prefixAlreadyWritten, prefixWidth, message, i, message.Length - i); return _stringBuilderProvider.GetStringAndRelease(sb); } /// <summary> /// Append aligned and indented message lines into running <see cref="StringBuilder"/>. /// </summary> private void AlignAndIndentLineOfMessage(StringBuilder sb, bool prefixAlreadyWritten, int prefixWidth, string message, int start, int count) { int bufferWidthMinusNewLine = _bufferWidth - 1; bool bufferIsLargerThanPrefix = bufferWidthMinusNewLine > prefixWidth; if (_alignMessages && bufferIsLargerThanPrefix && count > 0) { // If the buffer is larger then the prefix information (timestamp and key) then reformat the messages // Beginning index of string to be written int index = 0; // Loop until all the string has been sent to the console while (index < count) { // Position of virtual console cursor. // By simulating cursor position adjustment for tab characters '\t' we can compute // exact numbers of characters from source string to fit into Console.BufferWidth. int cursor = 0; // Write prefix if needed if ((!prefixAlreadyWritten || index > 0 || start > 0) && prefixWidth > 0) { sb.Append(' ', prefixWidth); } // We have to adjust cursor position whether the prefix has been already written or we wrote/indented it ourselves cursor += prefixWidth; // End index of string to be written (behind last character) int endIndex = index; while (cursor < bufferWidthMinusNewLine) { int remainingCharsToEndOfBuffer = Math.Min(bufferWidthMinusNewLine - cursor, count - endIndex); int nextTab = message.IndexOf('\t', start + endIndex, remainingCharsToEndOfBuffer); if (nextTab >= 0) { // Position before tab cursor += nextTab - (start + endIndex); // Move to next tab position cursor += ConsoleTabWidth - cursor % ConsoleTabWidth; // Move end index after the '\t' in preparation for following IndexOf '\t' endIndex += nextTab - (start + endIndex) + 1; } else { endIndex += remainingCharsToEndOfBuffer; break; } } sb.Append(message, start + index, endIndex - index); sb.AppendLine(); index = endIndex; } } else { // If there is not enough room just print the message out and let the console do the formatting if (!prefixAlreadyWritten && prefixWidth > 0) { sb.Append(' ', prefixWidth); } sb.Append(message, start, count); sb.AppendLine(); } } } }