|
// 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.IO;
using System.Text;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Telemetry;
using Microsoft.Build.Shared;
#nullable disable
namespace Microsoft.Build.Logging
{
/// <summary>
/// A specialization of the ConsoleLogger that logs to a file instead of the console.
/// The output in terms of what is written and how it looks is identical. For example you can
/// log verbosely to a file using the FileLogger while simultaneously logging only high priority events
/// to the console using a ConsoleLogger.
/// </summary>
/// <remarks>
/// It's unfortunate that this is derived from ConsoleLogger, which is itself a facade; it makes things more
/// complex -- for example, there is parameter parsing in this class, plus in BaseConsoleLogger. However we have
/// to derive FileLogger from ConsoleLogger because it shipped that way in Whidbey.
/// </remarks>
public class FileLogger : ConsoleLogger
{
#region Constructors
/// <summary>
/// Default constructor.
/// </summary>
public FileLogger()
: base(
LoggerVerbosity.Normal,
write: null, // Overwritten below
colorSet: BaseConsoleLogger.DontSetColor,
colorReset: BaseConsoleLogger.DontResetColor)
{
WriteHandler = Write;
if (EncodingUtilities.GetExternalOverriddenUILanguageIfSupportableWithEncoding() != null)
{
_encoding = Encoding.UTF8;
}
}
#endregion
/// <summary>
/// Signs up the console file logger for all build events.
/// This is the backward-compatible overload.
/// </summary>
/// <param name="eventSource">Available events.</param>
public override void Initialize(IEventSource eventSource)
{
ErrorUtilities.VerifyThrowArgumentNull(eventSource);
eventSource.BuildFinished += FileLoggerBuildFinished;
InitializeFileLogger(eventSource, 1);
}
private void FileLoggerBuildFinished(object sender, BuildFinishedEventArgs e)
{
_fileWriter?.Flush();
}
/// <summary>
/// Creates new file for logging
/// </summary>
private void InitializeFileLogger(IEventSource eventSource, int nodeCount)
{
// Prepend the default setting of "forcenoalign": no alignment is needed as we're
// writing to a file
string parameters = Parameters;
if (parameters != null)
{
Parameters = "FORCENOALIGN;" + parameters;
}
else
{
Parameters = "FORCENOALIGN;";
}
ParseFileLoggerParameters();
// Finally, ask the base console logger class to initialize. It may
// want to make decisions based on our verbosity, so we do this last.
base.Initialize(eventSource, nodeCount);
KnownTelemetry.LoggingConfigurationTelemetry.FileLoggersCount++;
KnownTelemetry.LoggingConfigurationTelemetry.FileLogger = true;
KnownTelemetry.LoggingConfigurationTelemetry.FileLoggerVerbosity = Verbosity.ToString();
if (!SkipProjectStartedText && Verbosity >= LoggerVerbosity.Normal)
{
eventSource.BuildStarted += (obj, args) => WriteHandler(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LogLoggerVerbosity", Verbosity));
}
try
{
string logDirectory = null;
try
{
logDirectory = Path.GetDirectoryName(Path.GetFullPath(_logFileName));
}
catch
{
// Directory creation is best-effort; if finding its path fails don't create the directory
// and possibly let OpenWrite() below report the failure
}
if (logDirectory != null)
{
Directory.CreateDirectory(logDirectory);
}
_fileWriter = FileUtilities.OpenWrite(_logFileName, _append, _encoding);
_fileWriter.AutoFlush = _autoFlush;
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidFileLoggerFile", _logFileName, e.Message);
_fileWriter?.Dispose();
throw new LoggerException(message, e.InnerException, errorCode, helpKeyword);
}
}
/// <summary>
/// Multiproc aware initialization
/// </summary>
public override void Initialize(IEventSource eventSource, int nodeCount)
{
InitializeFileLogger(eventSource, nodeCount);
}
/// <summary>
/// The handler for the write delegate of the console logger we are deriving from.
/// </summary>
/// <param name="text">The text to write to the log</param>
private void Write(string text)
{
try
{
_fileWriter.Write(text);
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidFileLoggerFile", _logFileName, ex.Message);
_fileWriter?.Dispose();
throw new LoggerException(message, ex.InnerException, errorCode, helpKeyword);
}
}
/// <summary>
/// Shutdown method implementation of ILogger - we need to flush and close our logfile.
/// </summary>
public override void Shutdown()
{
_fileWriter?.Dispose();
}
/// <summary>
/// Parses out the logger parameters from the Parameters string.
/// </summary>
private void ParseFileLoggerParameters()
{
if (Parameters == null)
{
return;
}
foreach (string parameter in Parameters.Split(s_fileLoggerParameterDelimiters))
{
if (parameter.Length <= 0)
{
continue;
}
var parameterAndValue = parameter.Split(s_fileLoggerParameterValueSplitCharacter);
ApplyFileLoggerParameter(parameterAndValue[0],
parameterAndValue.Length > 1 ? parameterAndValue[1] : null);
}
}
/// <summary>
/// Apply a parameter parsed by the file logger.
/// </summary>
private void ApplyFileLoggerParameter(string parameterName, string parameterValue)
{
switch (parameterName.ToUpperInvariant())
{
case "LOGFILE":
_logFileName = FileUtilities.FixFilePath(parameterValue);
break;
case "APPEND":
_append = true;
break;
case "NOAUTOFLUSH":
_autoFlush = false;
break;
case "ERRORSONLY":
case "WARNINGSONLY":
ShowSummary = false;
SkipProjectStartedText = true;
break;
case "ENCODING":
try
{
_encoding = Encoding.GetEncoding(parameterValue);
}
catch (ArgumentException ex)
{
// Can't change strings at this point, so for now we are using the exception string
// verbatim, and supplying a error code directly.
// This should move into the .resx later.
throw new LoggerException(ex.Message, ex.InnerException, "MSB4128", null);
}
break;
default:
// We will not error for unrecognized parameters, since someone may wish to
// extend this class and call this base method before theirs.
break;
}
}
#region Private member data
/// <summary>
/// logFileName is the name of the log file that we will generate
/// the default value is msbuild.log
/// </summary>
private string _logFileName = "msbuild.log";
/// <summary>
/// fileWriter is the stream that has been opened on our log file.
/// </summary>
private StreamWriter _fileWriter;
/// <summary>
/// Whether the logger should append to any existing file.
/// Default is to overwrite.
/// </summary>
private bool _append;
/// <summary>
/// Whether the logger should flush aggressively to disk.
/// Default is true. This preserves the most information in the case
/// of a crash, but may slow the logger down.
/// </summary>
private bool _autoFlush = true;
#if FEATURE_ENCODING_DEFAULT
/// <summary>
/// Encoding for the output. Defaults to ANSI.
/// </summary>
private Encoding _encoding = Encoding.Default;
#else
/// <summary>
/// Encoding for the output. Defaults to UTF-8.
/// </summary>
private Encoding _encoding = new UTF8Encoding(false);
#endif
/// <summary>
/// File logger parameters delimiters.
/// </summary>
private static readonly char[] s_fileLoggerParameterDelimiters = MSBuildConstants.SemicolonChar;
/// <summary>
/// File logger parameter value split character.
/// </summary>
private static readonly char[] s_fileLoggerParameterValueSplitCharacter = MSBuildConstants.EqualsChar;
#endregion
}
}
|