File: Logging\BaseConsoleLogger.cs
Web Access
Project: ..\..\..\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.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Logging;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using ColorResetter = Microsoft.Build.Logging.ColorResetter;
using ColorSetter = Microsoft.Build.Logging.ColorSetter;
using WriteHandler = Microsoft.Build.Logging.WriteHandler;
 
#nullable disable
 
namespace Microsoft.Build.BackEnd.Logging
{
    #region Delegates
    internal delegate void WriteLinePrettyFromResourceDelegate(int indentLevel, string resourceString, params object[] args);
    #endregion
 
    internal abstract class BaseConsoleLogger : INodeLogger, IStringBuilderProvider
    {
        #region Properties
 
        /// <summary>
        /// Gets or sets the level of detail to show in the event log.
        /// </summary>
        /// <value>Verbosity level.</value>
        public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Normal;
 
        /// <summary>
        /// Gets or sets the number of MSBuild processes participating in the build. If greater than 1,
        /// include the node ID
        /// </summary>
        public int NumberOfProcessors { get; set; } = 1;
 
        /// <summary>
        /// The console logger takes a single parameter to suppress the output of the errors
        /// and warnings summary at the end of a build.
        /// </summary>
        /// <value>null</value>
        public string Parameters { get; set; } = null;
 
        /// <summary>
        /// Suppresses the display of project headers. Project headers are
        /// displayed by default unless this property is set.
        /// </summary>
        /// <remarks>This is only needed by the IDE logger.</remarks>
        internal bool SkipProjectStartedText { get; set; } = false;
 
        /// <summary>
        /// Suppresses the display of error and warnings summary.
        /// If null, user has made no indication.
        /// </summary>
        internal bool? ShowSummary { get; set; }
 
        /// <summary>
        /// Provide access to the write hander delegate so that it can be redirected
        /// if necessary (e.g. to a file)
        /// </summary>
        protected internal WriteHandler WriteHandler { get; set; }
 
        #endregion
 
        /// <summary>
        /// Parses out the logger parameters from the Parameters string.
        /// </summary>
        public void ParseParameters()
        {
            foreach (var parameter in LoggerParametersHelper.ParseParameters(Parameters))
            {
                ApplyParameter(parameter.Item1, parameter.Item2);
            }
        }
 
        /// <summary>
        /// An implementation of IComparer useful for comparing the keys
        /// on DictionaryEntry's
        /// </summary>
        /// <remarks>Uses CurrentCulture for display purposes</remarks>
        internal class DictionaryEntryKeyComparer : IComparer<DictionaryEntry>
        {
            public static DictionaryEntryKeyComparer Instance { get; } = new();
 
            private DictionaryEntryKeyComparer() { }
 
            public int Compare(DictionaryEntry a, DictionaryEntry b)
            {
                return string.Compare((string)a.Key, (string)b.Key, StringComparison.CurrentCultureIgnoreCase);
            }
        }
 
        /// <summary>
        /// An implementation of IComparer useful for comparing the ItemSpecs
        /// on ITaskItem's
        /// </summary>
        /// <remarks>Uses CurrentCulture for display purposes</remarks>
        internal class ITaskItemItemSpecComparer : IComparer
        {
            public int Compare(Object a, Object b)
            {
                return String.Compare(
                    ((ITaskItem)a).ItemSpec,
                    ((ITaskItem)b).ItemSpec,
                    StringComparison.CurrentCultureIgnoreCase);
            }
        }
 
        /// <summary>
        /// Indents the given string by the specified number of spaces.
        /// </summary>
        /// <param name="s">String to indent.</param>
        /// <param name="indent">Depth to indent.</param>
        internal string IndentString(string s, int indent)
        {
            return OptimizedStringIndenter.IndentString(s, indent, (IStringBuilderProvider)this);
        }
 
        /// <summary>
        /// Splits strings on 'newLines' with tolerance for Everett and Dogfood builds.
        /// </summary>
        /// <param name="s">String to split.</param>
        internal static string[] SplitStringOnNewLines(string s)
        {
            string[] subStrings = s.Split(newLines, StringSplitOptions.None);
            return subStrings;
        }
 
        /// <summary>
        /// Writes a newline to the log.
        /// </summary>
        internal void WriteNewLine()
        {
            WriteHandler(Environment.NewLine);
        }
 
        /// <summary>
        /// Writes a line from a resource string to the log, using the default indentation.
        /// </summary>
        /// <param name="resourceString"></param>
        /// <param name="args"></param>
        internal void WriteLinePrettyFromResource(string resourceString, params object[] args)
        {
            int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? this.currentIndentLevel : 0;
            WriteLinePrettyFromResource(indentLevel, resourceString, args);
        }
 
        /// <summary>
        /// Writes a line from a resource string to the log, using the specified indentation.
        /// </summary>
        internal void WriteLinePrettyFromResource(int indentLevel, string resourceString, params object[] args)
        {
            string formattedString = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(resourceString, args);
            WriteLinePretty(indentLevel, formattedString);
        }
 
        /// <summary>
        /// Writes to the log, using the default indentation. Does not
        /// terminate with a newline.
        /// </summary>
        internal void WritePretty(string formattedString)
        {
            int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? this.currentIndentLevel : 0;
            WritePretty(indentLevel, formattedString);
        }
 
        /// <summary>
        /// If requested, display a performance summary at the end of the build.  This
        /// shows how much time (and # hits) were spent inside of each project, target,
        /// and task.
        /// </summary>
        internal void ShowPerfSummary()
        {
            if (projectEvaluationPerformanceCounters != null)
            {
                setColor(ConsoleColor.Green);
                WriteNewLine();
                WriteLinePrettyFromResource("ProjectEvaluationPerformanceSummary");
 
                setColor(ConsoleColor.Gray);
                DisplayCounters(projectEvaluationPerformanceCounters);
            }
 
            // Show project performance summary.
            if (projectPerformanceCounters != null)
            {
                setColor(ConsoleColor.Green);
                WriteNewLine();
                WriteLinePrettyFromResource("ProjectPerformanceSummary");
 
                setColor(ConsoleColor.Gray);
                DisplayCounters(projectPerformanceCounters);
            }
 
            // Show target performance summary.
            if (targetPerformanceCounters != null)
            {
                setColor(ConsoleColor.Green);
                WriteNewLine();
                WriteLinePrettyFromResource("TargetPerformanceSummary");
 
                setColor(ConsoleColor.Gray);
                DisplayCounters(targetPerformanceCounters);
            }
 
            // Show task performance summary.
            if (taskPerformanceCounters != null)
            {
                setColor(ConsoleColor.Green);
                WriteNewLine();
                WriteLinePrettyFromResource("TaskPerformanceSummary");
 
                setColor(ConsoleColor.Gray);
                DisplayCounters(taskPerformanceCounters);
            }
 
            resetColor();
        }
 
        /// <summary>
        /// Writes to the log, using the specified indentation. Does not
        /// terminate with a newline.
        /// </summary>
        internal void WritePretty(int indentLevel, string formattedString)
        {
            StringBuilder result = new StringBuilder((indentLevel * tabWidth) + formattedString.Length);
            result.Append(' ', indentLevel * tabWidth).Append(formattedString);
            WriteHandler(result.ToString());
        }
 
        /// <summary>
        /// Writes a line to the log, using the default indentation.
        /// </summary>
        /// <param name="formattedString"></param>
        internal void WriteLinePretty(string formattedString)
        {
            int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? currentIndentLevel : 0;
            WriteLinePretty(indentLevel, formattedString);
        }
 
        /// <summary>
        /// Writes a line to the log, using the specified indentation.
        /// </summary>
        internal void WriteLinePretty(int indentLevel, string formattedString)
        {
            indentLevel = indentLevel > 0 ? indentLevel : 0;
            WriteHandler(IndentString(formattedString, indentLevel * tabWidth));
        }
 
        /// <summary>
        /// Check to see what kind of device we are outputting the log to, is it a character device, a file, or something else
        /// this can be used by loggers to modify their outputs based on the device they are writing to
        /// </summary>
        internal void IsRunningWithCharacterFileType()
        {
            runningWithCharacterFileType = NativeMethodsShared.IsWindows && ConsoleConfiguration.OutputIsScreen;
        }
 
        /// <summary>
        /// Determines whether the current verbosity setting is at least the value
        /// passed in.
        /// </summary>
        internal bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity) => Verbosity >= checkVerbosity;
 
        /// <summary>
        /// Returns the minimum logger verbosity required to log a message with the given importance.
        /// </summary>
        /// <param name="importance">The message importance.</param>
        /// <param name="lightenText">True if the message should be rendered using lighter colored text.</param>
        /// <returns>The logger verbosity required to log a message of the given <paramref name="importance"/>.</returns>
        internal static LoggerVerbosity ImportanceToMinimumVerbosity(MessageImportance importance, out bool lightenText)
        {
            switch (importance)
            {
                case MessageImportance.High:
                    lightenText = false;
                    return LoggerVerbosity.Minimal;
                case MessageImportance.Normal:
                    lightenText = true;
                    return LoggerVerbosity.Normal;
                case MessageImportance.Low:
                    lightenText = true;
                    return LoggerVerbosity.Detailed;
 
                default:
                    ErrorUtilities.ThrowInternalError("Impossible");
                    lightenText = false;
                    return LoggerVerbosity.Detailed;
            }
        }
 
        /// <summary>
        /// Sets foreground color to color specified
        /// </summary>
        internal static void SetColor(ConsoleColor c)
        {
            try
            {
                Console.ForegroundColor = TransformColor(c, ConsoleConfiguration.BackgroundColor);
            }
            catch (IOException)
            {
                // Does not matter if we cannot set the color
            }
        }
 
        /// <summary>
        /// Resets the color
        /// </summary>
        internal static void ResetColor()
        {
            try
            {
                Console.ResetColor();
            }
            catch (IOException)
            {
                // The color could not be reset, no reason to crash
            }
        }
 
        /// <summary>
        /// Sets foreground color to color specified using ANSI escape codes
        /// </summary>
        /// <param name="c">foreground color</param>
        internal static void SetColorAnsi(ConsoleColor c)
        {
            string colorString = "\x1b[";
            switch (c)
            {
                case ConsoleColor.Black: colorString += "30"; break;
                case ConsoleColor.DarkBlue: colorString += "34"; break;
                case ConsoleColor.DarkGreen: colorString += "32"; break;
                case ConsoleColor.DarkCyan: colorString += "36"; break;
                case ConsoleColor.DarkRed: colorString += "31"; break;
                case ConsoleColor.DarkMagenta: colorString += "35"; break;
                case ConsoleColor.DarkYellow: colorString += "33"; break;
                case ConsoleColor.Gray: colorString += "37"; break;
                case ConsoleColor.DarkGray: colorString += "30;1"; break;
                case ConsoleColor.Blue: colorString += "34;1"; break;
                case ConsoleColor.Green: colorString += "32;1"; break;
                case ConsoleColor.Cyan: colorString += "36;1"; break;
                case ConsoleColor.Red: colorString += "31;1"; break;
                case ConsoleColor.Magenta: colorString += "35;1"; break;
                case ConsoleColor.Yellow: colorString += "33;1"; break;
                case ConsoleColor.White: colorString += "37;1"; break;
                default: colorString = ""; break;
            }
            if ("" != colorString)
            {
                colorString += "m";
                Console.Out.Write(colorString);
            }
        }
 
        /// <summary>
        /// Resets the color using ANSI escape codes
        /// </summary>
        internal static void ResetColorAnsi()
        {
            Console.Out.Write("\x1b[m");
        }
 
        /// <summary>
        /// Changes the foreground color to black if the foreground is the
        /// same as the background. Changes the foreground to white if the
        /// background is black.
        /// </summary>
        /// <param name="foreground">foreground color for black</param>
        /// <param name="background">current background</param>
        internal static ConsoleColor TransformColor(ConsoleColor foreground, ConsoleColor background)
        {
            ConsoleColor result = foreground; // typically do nothing ...
 
            if (foreground == background)
            {
                result = background != ConsoleColor.Black ? ConsoleColor.Black : ConsoleColor.Gray;
            }
 
            return result;
        }
 
        /// <summary>
        /// Does nothing, meets the ColorSetter delegate type
        /// </summary>
        /// <param name="c">foreground color (is ignored)</param>
        internal static void DontSetColor(ConsoleColor c)
        {
            // do nothing...
        }
 
        /// <summary>
        /// Does nothing, meets the ColorResetter delegate type
        /// </summary>
        internal static void DontResetColor()
        {
            // do nothing...
        }
 
        internal void InitializeConsoleMethods(LoggerVerbosity logverbosity, WriteHandler logwriter, ColorSetter colorSet, ColorResetter colorReset)
        {
            Verbosity = logverbosity;
            WriteHandler = logwriter;
            IsRunningWithCharacterFileType();
            // This is a workaround, because the Console class provides no way to check that a color
            // can actually be set or not. Color cannot be set if the console has been redirected
            // in certain ways (e.g. how BUILD.EXE does it)
            bool canSetColor = true;
 
            try
            {
                ConsoleColor c = ConsoleConfiguration.BackgroundColor;
            }
            catch (IOException)
            {
                // If the attempt to set a color fails with an IO exception then it is
                // likely that the console has been redirected in a way that cannot
                // cope with color (e.g. BUILD.EXE) so don't try to do color again.
                canSetColor = false;
            }
 
            if (colorSet != null && canSetColor)
            {
                this.setColor = colorSet;
            }
            else
            {
                this.setColor = DontSetColor;
            }
 
            if (colorReset != null && canSetColor)
            {
                this.resetColor = colorReset;
            }
            else
            {
                this.resetColor = DontResetColor;
            }
        }
 
        /// <summary>
        /// Writes out the list of property names and their values.
        /// This could be done at any time during the build to show the latest
        /// property values, using the cached reference to the list from the
        /// appropriate ProjectStarted event.
        /// </summary>
        /// <param name="properties">List of properties</param>
        internal void WriteProperties(List<DictionaryEntry> properties)
        {
            if (Verbosity == LoggerVerbosity.Diagnostic && showItemAndPropertyList)
            {
                if (properties.Count == 0)
                {
                    return;
                }
 
                OutputProperties(properties);
                // Add a blank line
                WriteNewLine();
            }
        }
 
        /// <summary>
        /// Writes out the environment as seen on build started.
        /// </summary>
        internal void WriteEnvironment(IDictionary<string, string> environment)
        {
            if (environment == null || environment.Count == 0)
            {
                return;
            }
 
            if (Verbosity == LoggerVerbosity.Diagnostic || showEnvironment)
            {
                OutputEnvironment(environment);
 
                // Add a blank line
                WriteNewLine();
            }
        }
 
        /// <summary>
        /// Generate a list which contains the properties referenced by the properties
        /// enumerable object
        /// </summary>
        internal List<DictionaryEntry> ExtractPropertyList(IEnumerable properties)
        {
            // Gather a sorted list of all the properties.
            var list = new List<DictionaryEntry>(properties.FastCountOrZero());
 
            Internal.Utilities.EnumerateProperties(properties, list, static (list, kvp) => list.Add(new DictionaryEntry(kvp.Key, kvp.Value)));
 
            list.Sort(DictionaryEntryKeyComparer.Instance);
            return list;
        }
 
        /// <summary>
        /// Write the environment of the build as was captured on the build started event.
        /// </summary>
        internal virtual void OutputEnvironment(IDictionary<string, string> environment)
        {
            // Write the banner
            setColor(ConsoleColor.Green);
            WriteLinePretty(currentIndentLevel, ResourceUtilities.GetResourceString("EnvironmentHeader"));
 
            if (environment != null)
            {
                // Write each environment value one per line
                foreach (KeyValuePair<string, string> entry in environment)
                {
                    setColor(ConsoleColor.Gray);
                    WritePretty(String.Format(CultureInfo.CurrentCulture, "{0,-30} = ", entry.Key));
                    setColor(ConsoleColor.DarkGray);
                    WriteLinePretty(entry.Value);
                }
            }
 
            resetColor();
        }
 
        internal virtual void OutputProperties(List<DictionaryEntry> list)
        {
            // Write the banner
            setColor(ConsoleColor.Green);
            WriteLinePretty(currentIndentLevel, ResourceUtilities.GetResourceString("PropertyListHeader"));
            // Write each property name and its value, one per line
            foreach (DictionaryEntry prop in list)
            {
                setColor(ConsoleColor.Gray);
                WritePretty(String.Format(CultureInfo.CurrentCulture, "{0,-30} = ", prop.Key));
                setColor(ConsoleColor.DarkGray);
                WriteLinePretty(EscapingUtilities.UnescapeAll((string)prop.Value));
            }
            resetColor();
        }
 
        /// <summary>
        /// Writes out the list of item specs and their metadata.
        /// This could be done at any time during the build to show the latest
        /// items, using the cached reference to the list from the
        /// appropriate ProjectStarted event.
        /// </summary>
        internal void WriteItems(SortedList itemTypes)
        {
            if (Verbosity != LoggerVerbosity.Diagnostic || !showItemAndPropertyList || itemTypes.Count == 0)
            {
                return;
            }
 
            // Write the banner
            setColor(ConsoleColor.Green);
            WriteLinePretty(currentIndentLevel, ResourceUtilities.GetResourceString("ItemListHeader"));
 
            // Write each item type and its itemspec, one per line
            foreach (DictionaryEntry entry in itemTypes)
            {
                string itemType = (string)entry.Key;
                ArrayList itemTypeList = (ArrayList)entry.Value;
 
                if (itemTypeList.Count == 0)
                {
                    continue;
                }
 
                // Sort the list by itemSpec
                itemTypeList.Sort(new ITaskItemItemSpecComparer());
                OutputItems(itemType, itemTypeList);
            }
 
            // Add a blank line
            WriteNewLine();
        }
 
        /// <summary>
        /// Extract the Items from the enumerable object and return a sorted list containing these items
        /// </summary>
        internal SortedList ExtractItemList(IEnumerable items)
        {
            // The "items" list is a flat list of itemtype-ITaskItem pairs.
            // We would like to organize the ITaskItems into groups by itemtype.
 
            // Use a SortedList instead of an ArrayList (because we need to lookup fast)
            // and instead of a Hashtable (because we need to sort it)
            SortedList itemTypes = new SortedList(CaseInsensitiveComparer.Default);
 
            Internal.Utilities.EnumerateItems(items, item =>
            {
                string key = (string)item.Key;
                var bucket = itemTypes[key] as ArrayList;
                if (bucket == null)
                {
                    bucket = new ArrayList();
                    itemTypes[key] = bucket;
                }
 
                bucket.Add(item.Value);
            });
 
            return itemTypes;
        }
 
        /// <summary>
        /// Dump the initial items provided.
        /// Overridden in ParallelConsoleLogger.
        /// </summary>
        internal virtual void OutputItems(string itemType, ArrayList itemTypeList)
        {
            WriteItemType(itemType);
 
            foreach (var item in itemTypeList)
            {
                string itemSpec = item switch
                {
                    ITaskItem taskItem => taskItem.ItemSpec,
                    IItem iitem => iitem.EvaluatedInclude,
                    { } misc => Convert.ToString(misc),
                    null => "null"
                };
 
                WriteItemSpec(itemSpec);
 
                var metadata = item switch
                {
                    IMetadataContainer metadataContainer => metadataContainer.EnumerateMetadata(),
                    IItem<ProjectMetadata> iitem => iitem.Metadata?.Select(m => new KeyValuePair<string, string>(m.Name, m.EvaluatedValue)),
                    _ => null
                };
 
                if (metadata != null)
                {
                    foreach (var metadatum in metadata)
                    {
                        WriteMetadata(metadatum.Key, metadatum.Value);
                    }
                }
            }
 
            resetColor();
        }
 
        protected virtual void WriteItemType(string itemType)
        {
            setColor(ConsoleColor.Gray);
            WriteLinePretty(itemType);
            setColor(ConsoleColor.DarkGray);
        }
 
        protected virtual void WriteItemSpec(string itemSpec)
        {
            WriteLinePretty("    " + itemSpec);
        }
 
        protected virtual void WriteMetadata(string name, string value)
        {
            WriteLinePretty("        " + name + " = " + value);
        }
 
        /// <summary>
        /// Returns a performance counter for a given scope (either task name or target name)
        /// from the given table.
        /// </summary>
        /// <param name="scopeName">Task name or target name.</param>
        /// <param name="table">Table that has tasks or targets.</param>
        /// <returns></returns>
        internal static PerformanceCounter GetPerformanceCounter(string scopeName, ref Dictionary<string, PerformanceCounter> table)
        {
            // Lazily construct the performance counter table.
            if (table == null)
            {
                table = new Dictionary<string, PerformanceCounter>(StringComparer.OrdinalIgnoreCase);
            }
 
            // And lazily construct the performance counter itself.
            PerformanceCounter counter;
            if (!table.TryGetValue(scopeName, out counter))
            {
                counter = new PerformanceCounter(scopeName);
                table[scopeName] = counter;
            }
 
            return counter;
        }
 
        /// <summary>
        /// Display the timings for each counter in the dictionary.
        /// </summary>
        /// <param name="counters"></param>
        internal void DisplayCounters(Dictionary<string, PerformanceCounter> counters)
        {
            ArrayList perfCounters = new ArrayList(counters.Values.Count);
            perfCounters.AddRange(counters.Values);
 
            perfCounters.Sort(PerformanceCounter.DescendingByElapsedTimeComparer);
 
            bool reentrantCounterExists = false;
 
            WriteLinePrettyFromResourceDelegate lineWriter = WriteLinePrettyFromResource;
 
            foreach (PerformanceCounter counter in perfCounters)
            {
                if (counter.ReenteredScope)
                {
                    reentrantCounterExists = true;
                }
 
                counter.PrintCounterMessage(lineWriter, setColor, resetColor);
            }
 
            if (reentrantCounterExists)
            {
                // display an explanation of why there was no value displayed
                WriteLinePrettyFromResource(4, "PerformanceReentrancyNote");
            }
        }
 
        /// <summary>
        /// Records performance information consumed by a task or target.
        /// </summary>
        internal class PerformanceCounter
        {
            protected string scopeName;
            protected int calls;
            protected TimeSpan elapsedTime = new TimeSpan(0);
            protected bool inScope;
            protected DateTime scopeStartTime;
            protected bool reenteredScope;
 
            /// <summary>
            /// Construct.
            /// </summary>
            /// <param name="scopeName"></param>
            internal PerformanceCounter(string scopeName)
            {
                this.scopeName = scopeName;
            }
 
            /// <summary>
            /// Name of the scope.
            /// </summary>
            internal string ScopeName => scopeName;
 
            /// <summary>
            /// Total number of calls so far.
            /// </summary>
            internal int Calls => calls;
 
            /// <summary>
            /// Total accumulated time so far.
            /// </summary>
            internal TimeSpan ElapsedTime => elapsedTime;
 
            /// <summary>
            /// Whether or not this scope was reentered. Timing information is not recorded in these cases.
            /// </summary>
            internal bool ReenteredScope => reenteredScope;
 
            /// <summary>
            /// Whether or not this task or target is executing right now.
            /// </summary>
            internal bool InScope
            {
                get
                {
                    return inScope;
                }
 
                set
                {
                    if (!reenteredScope)
                    {
                        if (InScope && !value)
                        {
                            // Edge meaning scope is finishing.
                            inScope = false;
 
                            elapsedTime += (DateTime.Now - scopeStartTime);
                        }
                        else if (!InScope && value)
                        {
                            // Edge meaning scope is starting.
                            inScope = true;
 
                            ++calls;
                            scopeStartTime = DateTime.Now;
                        }
                        else
                        {
                            // Should only happen when a scope is reentrant.
                            // We don't track these numbers.
                            reenteredScope = true;
                        }
                    }
                }
            }
 
            internal virtual void PrintCounterMessage(WriteLinePrettyFromResourceDelegate writeLinePrettyFromResource, ColorSetter setColor, ColorResetter resetColor)
            {
                string time;
                if (!reenteredScope)
                {
                    // round: sub-millisecond values are not meaningful
                    time = String.Format(CultureInfo.CurrentCulture,
                        "{0,5}", Math.Round(elapsedTime.TotalMilliseconds, 0));
                }
                else
                {
                    // no value available; instead display an asterisk
                    time = "    *";
                }
 
                writeLinePrettyFromResource(
                    2,
                    "PerformanceLine",
                    time,
                    String.Format(CultureInfo.CurrentCulture, "{0,-40}" /* pad to 40 align left */, scopeName),
                    String.Format(CultureInfo.CurrentCulture, "{0,3}", calls));
            }
 
            /// <summary>
            /// Returns an IComparer that will put performance counters
            /// in descending order by elapsed time.
            /// </summary>
            internal static IComparer DescendingByElapsedTimeComparer => new DescendingByElapsedTime();
 
            /// <summary>
            /// Private IComparer class for sorting performance counters
            /// in descending order by elapsed time.
            /// </summary>
            internal class DescendingByElapsedTime : IComparer
            {
                /// <summary>
                /// Compare two PerformanceCounters.
                /// </summary>
                /// <param name="o1"></param>
                /// <param name="o2"></param>
                /// <returns></returns>
                public int Compare(object o1, object o2)
                {
                    PerformanceCounter p1 = (PerformanceCounter)o1;
                    PerformanceCounter p2 = (PerformanceCounter)o2;
 
                    // don't compare reentrant counters, time is incorrect
                    // and we want to sort them first
                    if (!p1.reenteredScope && !p2.reenteredScope)
                    {
                        return TimeSpan.Compare(p1.ElapsedTime, p2.ElapsedTime);
                    }
                    else if (p1.Equals(p2))
                    {
                        return 0;
                    }
                    else if (p1.reenteredScope && !p2.reenteredScope)
                    {
                        // p1 was reentrant; sort first
                        return -1;
                    }
                    else if (!p1.reenteredScope && p2.reenteredScope)
                    {
                        // p2 was reentrant; sort first
                        return 1;
                    }
                    else
                    {
                        // both reentrant; sort stably by another field to avoid throwing
                        return string.Compare(p1.ScopeName, p2.ScopeName, StringComparison.Ordinal);
                    }
                }
            }
        }
 
        #region eventHandlers
 
        public virtual void Shutdown()
        {
            Traits.LogAllEnvironmentVariables = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDLOGALLENVIRONMENTVARIABLES"));
        }
 
        internal abstract void ResetConsoleLoggerState();
 
        public virtual void Initialize(IEventSource eventSource, int nodeCount)
        {
            NumberOfProcessors = nodeCount;
            Initialize(eventSource);
        }
 
        /// <summary>
        /// Signs up the console logger for all build events.
        /// </summary>
        /// <param name="eventSource">Available events.</param>
        public virtual void Initialize(IEventSource eventSource)
        {
            // Always show perf summary for diagnostic verbosity.
            if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic))
            {
                this.showPerfSummary = true;
            }
 
            ParseParameters();
 
            showTargetOutputs = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING"));
 
            if (showOnlyWarnings || showOnlyErrors)
            {
                if (ShowSummary == null)
                {
                    // By default don't show the summary when the showOnlyWarnings / showOnlyErrors is specified.
                    // However, if the user explicitly specified summary or nosummary, use that.
                    ShowSummary = false;
                }
 
                this.showPerfSummary = false;
            }
 
            // If not specifically instructed otherwise, show a summary in normal
            // and higher verbosities.
            if (ShowSummary == null && IsVerbosityAtLeast(LoggerVerbosity.Normal))
            {
                ShowSummary = true;
            }
 
            // Put this after reading the parameters, since it may want to initialize something
            // specially based on some parameter value. For example, choose whether to have a summary, based
            // on the verbosity.
            ResetConsoleLoggerState();
 
            // Event source is allowed to be null; this allows the logger to be wrapped by a class that wishes
            // to call its event handlers directly. The VS HostLogger does this.
            if (eventSource != null)
            {
                eventSource.BuildStarted += BuildStartedHandler;
                eventSource.BuildFinished += BuildFinishedHandler;
                eventSource.ProjectStarted += ProjectStartedHandler;
                eventSource.ProjectFinished += ProjectFinishedHandler;
                eventSource.TargetStarted += TargetStartedHandler;
                eventSource.TargetFinished += TargetFinishedHandler;
                eventSource.TaskStarted += TaskStartedHandler;
                eventSource.TaskFinished += TaskFinishedHandler;
                eventSource.ErrorRaised += ErrorHandler;
                eventSource.WarningRaised += WarningHandler;
                eventSource.MessageRaised += MessageHandler;
                eventSource.CustomEventRaised += CustomEventHandler;
                eventSource.StatusEventRaised += StatusEventHandler;
 
                bool logPropertiesAndItemsAfterEvaluation = Traits.Instance.EscapeHatches.LogPropertiesAndItemsAfterEvaluation ?? true;
                if (logPropertiesAndItemsAfterEvaluation && eventSource is IEventSource4 eventSource4)
                {
                    eventSource4.IncludeEvaluationPropertiesAndItems();
                }
            }
        }
 
        /// <summary>
        /// Apply a logger parameter.
        /// parameterValue may be null, if there is no parameter value.
        /// </summary>
        internal virtual bool ApplyParameter(string parameterName, string parameterValue)
        {
            ErrorUtilities.VerifyThrowArgumentNull(parameterName, nameof(parameterName));
 
            switch (parameterName.ToUpperInvariant())
            {
                case "PERFORMANCESUMMARY":
                    showPerfSummary = true;
                    return true;
                case "NOPERFORMANCESUMMARY":
                    showPerfSummary = false;
                    return true;
                case "NOSUMMARY":
                    ShowSummary = false;
                    return true;
                case "SUMMARY":
                    ShowSummary = true;
                    return true;
                case "NOITEMANDPROPERTYLIST":
                    showItemAndPropertyList = false;
                    return true;
                case "WARNINGSONLY":
                    showOnlyWarnings = true;
                    return true;
                case "ERRORSONLY":
                    showOnlyErrors = true;
                    return true;
                case "SHOWENVIRONMENT":
                    showEnvironment = true;
                    Traits.LogAllEnvironmentVariables = true;
                    return true;
                case "SHOWPROJECTFILE":
                    if (parameterValue == null)
                    {
                        showProjectFile = true;
                    }
                    else
                    {
                        if (parameterValue.Length == 0)
                        {
                            showProjectFile = true;
                        }
                        else
                        {
                            showProjectFile = (parameterValue.ToUpperInvariant()) switch
                            {
                                "TRUE" => true,
                                _ => false,
                            };
                        }
                    }
 
                    return true;
                case "V":
                case "VERBOSITY":
                    return ApplyVerbosityParameter(parameterValue);
            }
 
            return false;
        }
 
        /// <summary>
        /// Apply the verbosity value
        /// </summary>
        private bool ApplyVerbosityParameter(string parameterValue)
        {
            if (LoggerParametersHelper.TryParseVerbosityParameter(parameterValue, out LoggerVerbosity? verbosity))
            {
                Verbosity = (LoggerVerbosity)verbosity;
                return true;
            }
            else
            {
                string errorCode;
                string helpKeyword;
                string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidVerbosity", parameterValue);
                throw new LoggerException(message, null, errorCode, helpKeyword);
            }
        }
 
        public abstract void BuildStartedHandler(object sender, BuildStartedEventArgs e);
 
        public abstract void BuildFinishedHandler(object sender, BuildFinishedEventArgs e);
 
        public abstract void ProjectStartedHandler(object sender, ProjectStartedEventArgs e);
 
        public abstract void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e);
 
        public abstract void TargetStartedHandler(object sender, TargetStartedEventArgs e);
 
        public abstract void TargetFinishedHandler(object sender, TargetFinishedEventArgs e);
 
        public abstract void TaskStartedHandler(object sender, TaskStartedEventArgs e);
 
        public abstract void TaskFinishedHandler(object sender, TaskFinishedEventArgs e);
 
        public abstract void ErrorHandler(object sender, BuildErrorEventArgs e);
 
        public abstract void WarningHandler(object sender, BuildWarningEventArgs e);
 
        public abstract void MessageHandler(object sender, BuildMessageEventArgs e);
 
        public abstract void CustomEventHandler(object sender, CustomBuildEventArgs e);
 
        public abstract void StatusEventHandler(object sender, BuildStatusEventArgs e);
 
        #endregion
 
        #region Internal member data
 
        /// <summary>
        /// Time the build started
        /// </summary>
        internal DateTime buildStarted;
 
        /// <summary>
        /// Delegate used to change text color.
        /// </summary>
        internal ColorSetter setColor = null;
 
        /// <summary>
        /// Delegate used to reset text color
        /// </summary>
        internal ColorResetter resetColor = null;
 
        /// <summary>
        /// Number of spaces that each level of indentation is worth
        /// </summary>
        internal const int tabWidth = 2;
 
        /// <summary>
        /// Keeps track of the current indentation level.
        /// </summary>
        internal int currentIndentLevel = 0;
 
        /// <summary>
        /// The kinds of newline breaks we expect.
        /// </summary>
        /// <remarks>Currently we're not supporting "\r".</remarks>
        internal static readonly string[] newLines = { "\r\n", "\n" };
 
        /// <summary>
        /// Visual separator for projects. Line length was picked arbitrarily.
        /// </summary>
        internal const string projectSeparatorLine =
                 "__________________________________________________";
 
        /// <summary>
        /// When true, accumulate performance numbers.
        /// </summary>
        internal bool showPerfSummary = false;
 
        /// <summary>
        /// When true, show the list of item and property values at the start of each project
        /// </summary>
        internal bool showItemAndPropertyList = true;
 
        /// <summary>
        /// Should the target output items be displayed
        /// </summary>
        internal bool showTargetOutputs = false;
 
        /// <summary>
        /// When true, suppresses all messages except for warnings. (And possibly errors, if showOnlyErrors is true.)
        /// </summary>
        protected bool showOnlyWarnings;
 
        /// <summary>
        /// When true, suppresses all messages except for errors. (And possibly warnings, if showOnlyWarnings is true.)
        /// </summary>
        protected bool showOnlyErrors;
 
        /// <summary>
        /// When true the environment block supplied by the build started event should be printed out at the start of the build
        /// </summary>
        protected bool showEnvironment;
 
        /// <summary>
        /// When true, indicates that the logger should tack the project file onto the end of errors and warnings.
        /// </summary>
        protected bool showProjectFile = false;
 
        internal bool ignoreLoggerErrors = true;
 
        internal bool runningWithCharacterFileType = false;
 
        /// <summary>
        /// Since logging messages are processed serially, we can use a single StringBuilder wherever needed.
        /// It should not be done directly, but rather through the <see cref="IStringBuilderProvider"/> interface methods.
        /// </summary>
        private StringBuilder _sharedStringBuilder = new StringBuilder(0x100);
 
        #endregion
 
        #region Per-build Members
 
        /// <summary>
        /// Number of errors encountered in this build
        /// </summary>
        internal int errorCount = 0;
 
        /// <summary>
        /// Number of warnings encountered in this build
        /// </summary>
        internal int warningCount = 0;
 
        /// <summary>
        /// A list of the errors that have occurred during this build.
        /// </summary>
        internal List<BuildErrorEventArgs> errorList;
 
        /// <summary>
        /// A list of the warnings that have occurred during this build.
        /// </summary>
        internal List<BuildWarningEventArgs> warningList;
 
        /// <summary>
        /// Accumulated project performance information.
        /// </summary>
        internal Dictionary<string, PerformanceCounter> projectPerformanceCounters;
 
        /// <summary>
        /// Accumulated target performance information.
        /// </summary>
        internal Dictionary<string, PerformanceCounter> targetPerformanceCounters;
 
        /// <summary>
        /// Accumulated task performance information.
        /// </summary>
        internal Dictionary<string, PerformanceCounter> taskPerformanceCounters;
 
        /// <summary>
        ///
        /// </summary>
        internal Dictionary<string, PerformanceCounter> projectEvaluationPerformanceCounters;
 
        #endregion
 
        /// <summary>
        /// Since logging messages are processed serially, we can reuse a single StringBuilder wherever needed.
        /// </summary>
        StringBuilder IStringBuilderProvider.Acquire(int capacity)
        {
            StringBuilder shared = Interlocked.Exchange(ref _sharedStringBuilder, null);
 
            Debug.Assert(shared != null, "This is not supposed to be used in multiple threads or multiple time. One method is expected to return it before next acquire. Most probably it was not returned.");
            if (shared == null)
            {
                // This is not supposed to be used concurrently. One method is expected to return it before next acquire.
                // However to avoid bugs in production, we will create new string builder
                return StringBuilderCache.Acquire(capacity);
            }
 
            if (shared.Capacity < capacity)
            {
                const int minimumCapacity = 0x100; // 256 characters, 512 bytes
                const int maximumBracketedCapacity = 0x80_000; // 512K characters, 1MB
 
                if (capacity <= minimumCapacity)
                {
                    capacity = minimumCapacity;
                }
                else if (capacity < maximumBracketedCapacity)
                {
                    // GC likes arrays allocated with power of two bytes. Lets make it happy.
 
                    // Find next power of two http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
                    int v = capacity;
 
                    v--;
                    v |= v >> 1;
                    v |= v >> 2;
                    v |= v >> 4;
                    v |= v >> 8;
                    v |= v >> 16;
                    v++;
 
                    capacity = v;
                }
                // If capacity is > maximumCapacity we will respect it and use it as is.
 
                // Lets create new instance with enough capacity.
                shared = new StringBuilder(capacity);
            }
 
            // Prepare for next use.
            // Equivalent of sb.Clear() that works on .Net 3.5
            shared.Length = 0;
 
            return shared;
        }
 
        /// <summary>
        /// Acquired StringBuilder must be returned before next use.
        /// Unbalanced releases are not supported.
        /// </summary>
        string IStringBuilderProvider.GetStringAndRelease(StringBuilder builder)
        {
            // This is not supposed to be used concurrently. One method is expected to return it before next acquire.
            // But just for sure if _sharedBuilder was already returned, keep the former.
            StringBuilder previous = Interlocked.CompareExchange(ref _sharedStringBuilder, builder, null);
            Debug.Assert(previous == null, "This is not supposed to be used in multiple threads or multiple time. One method is expected to return it before next acquire. Most probably it was double returned.");
 
            return builder.ToString();
        }
    }
}