diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index a6cc40c51b4..bd8f613625e 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -29,6 +29,9 @@ Change wave checks around features will be removed in the release that accompani ## Current Rotation of Change Waves +### 18.8 +- [Console, parallel console, and terminal loggers print the paths of log files written by registered loggers (e.g. file logger and binary logger) as part of the end-of-build summary.](https://github.com/dotnet/msbuild/pull/13577) + ### 18.7 - [Fix ASP.NET WebSite projects to resolve netstandard2.0 dependencies](https://github.com/dotnet/msbuild/pull/13058) - Pass TargetFrameworkVersion to RAR task and copy netstandard.dll facade for .NET Framework 4.7.1+ web projects. diff --git a/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs b/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs index 3fe5fcd8e6e..75a0ec1f9ad 100644 --- a/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs +++ b/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs @@ -14,6 +14,7 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Microsoft.Build.Logging; using Shouldly; using Xunit; using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; @@ -1046,6 +1047,53 @@ public void LogBuildFinished() buildEvent = new BuildFinishedEventArgs(string.Empty, null /* no help keyword */, true, service.ProcessedBuildEvent.Timestamp); Assert.True(((BuildFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); } + [Fact] + public void LogBuildStartedLoggerNames() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + ConsoleLogger consoleLogger = new ConsoleLogger(); + service.RegisterLogger(consoleLogger); + + service.LogBuildStarted(); + var enabledLogsEvent = service.AllProcessedBuildEvents + .OfType() + .FirstOrDefault(e => e.Message?.Contains("ConsoleLogger") == true); + enabledLogsEvent.ShouldNotBeNull(); + } + + [Fact] + public void LogFilePathsPresentInFileLog() + { + using var env = TestEnvironment.Create(); + var logFilePath = env.ExpectFile(".log").Path; + + var fileLogger = new FileLogger { Parameters = "logfile=" + logFilePath }; + var mockLogger = new MockLogger(); + + using (var collection = new ProjectCollection()) + { + var project = ObjectModelHelpers.CreateInMemoryProject(collection, @" + + + "); + project.Build(new ILogger[] { fileLogger, mockLogger }).ShouldBeTrue(); + } + + // Check that MockLogger captured a LoggersRegisteredEventArgs containing the file logger path + var registeredEvent = mockLogger.AllBuildEvents + .OfType() + .FirstOrDefault(e => e.Loggers.Any(l => l.LoggerName == nameof(FileLogger))); + registeredEvent.ShouldNotBeNull(); + var fileLoggerDesc = registeredEvent.Loggers.First(l => l.LoggerName == nameof(FileLogger)); + var expectedPath = Path.GetFullPath(logFilePath); + fileLoggerDesc.OutputFilePaths.ShouldContain(expectedPath); + fileLoggerDesc.LoggerTypeFullName.ShouldBe(typeof(FileLogger).FullName); + fileLoggerDesc.Parameters.ShouldBe(fileLogger.Parameters); + + // Check the file log itself contains the exact path + var fileLogContents = File.ReadAllText(logFilePath); + fileLogContents.ShouldContain(expectedPath); + } [Fact] public void LogBuildCanceled() @@ -1795,6 +1843,11 @@ internal sealed class ProcessBuildEventHelper : LoggingService /// to verify that a buildEvent was sent to ProcessLoggingEvent. /// private BuildEventArgs _processedBuildEvent; + + /// + /// All events processed by ProcessLoggingEvent. + /// + internal List AllProcessedBuildEvents { get; } = new(); #endregion #region Constructor /// @@ -1857,6 +1910,7 @@ protected internal override void ProcessLoggingEvent(object buildEvent) if (buildEvent is BuildEventArgs buildEventArgs) { _processedBuildEvent = buildEventArgs; + AllProcessedBuildEvents.Add(buildEventArgs); } else if (buildEvent is KeyValuePair kvp) { diff --git a/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs b/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs index 8ca50416de7..79594cde9c1 100644 --- a/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs +++ b/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs @@ -82,6 +82,7 @@ public void VerifyEventType() BuildCheckTracingEventArgs buildCheckTracing = new(); BuildCanceledEventArgs buildCanceled = new("message", DateTime.UtcNow); WorkerNodeTelemetryEventArgs workerNodeTelemetry = new(); + LoggersRegisteredEventArgs loggersRegistered = new(new List { new RegisteredLoggerInfo("FileLogger", new[] { @"C:\logs\build.log" }) }); VerifyLoggingPacket(buildFinished, LoggingEventType.BuildFinishedEvent); VerifyLoggingPacket(buildStarted, LoggingEventType.BuildStartedEvent); @@ -119,6 +120,7 @@ public void VerifyEventType() VerifyLoggingPacket(buildCheckTracing, LoggingEventType.BuildCheckTracingEvent); VerifyLoggingPacket(buildCanceled, LoggingEventType.BuildCanceledEvent); VerifyLoggingPacket(workerNodeTelemetry, LoggingEventType.WorkerNodeTelemetryEvent); + VerifyLoggingPacket(loggersRegistered, LoggingEventType.LoggersRegisteredEvent); } private static BuildEventContext CreateBuildEventContext() @@ -321,6 +323,11 @@ public void TestTranslation() BuildEventContext = new BuildEventContext(1, 2, 3, 4, 5, 6, 7) }, new GeneratedFileUsedEventArgs("path", "some content"), + new LoggersRegisteredEventArgs(new List + { + new RegisteredLoggerInfo("FileLogger", new[] { @"C:\logs\build.log" }), + new RegisteredLoggerInfo("BinaryLogger"), + }), }; foreach (BuildEventArgs arg in testArgs) { diff --git a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs index fe19764230c..489b7b67d98 100644 --- a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs +++ b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs @@ -68,7 +68,7 @@ public void InvalidCacheFilesShouldLogError(byte[] cacheContents) result.OverallResult.ShouldBe(BuildResultCode.Failure); _logger.FullLog.ShouldContain("MSB4256:"); - _logger.AllBuildEvents.Count.ShouldBe(6); + _logger.AllBuildEvents.Count.ShouldBe(8); _logger.ErrorCount.ShouldBe(1); } @@ -564,8 +564,8 @@ public void NonExistingInputResultsCacheShouldLogError() result.OverallResult.ShouldBe(BuildResultCode.Failure); - _logger.AllBuildEvents.Count.ShouldBe(6); - _logger.Errors.First().Message.ShouldContain("MSB4255:"); + _logger.AllBuildEvents.Count.ShouldBe(8); + _logger.Errors.First().Message.ShouldContain("MSB4255:"); _logger.Errors.First().Message.ShouldContain("FileDoesNotExist1"); _logger.Errors.First().Message.ShouldContain("FileDoesNotExist2"); _logger.ErrorCount.ShouldBe(1); diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs index 31c9f55eeb0..752b4844a2c 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -9,6 +9,7 @@ using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; +using Microsoft.Build.Logging; using Microsoft.Build.Shared; using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; @@ -360,6 +361,12 @@ public void LogBuildStarted() // Make sure we process this event before going any further WaitForLoggingToProcessEvents(); + + // Register Loggers and print out all the enabled loggers. + if (!OnlyLogCriticalEvents) + { + LogAndRegisterLoggers(); + } } /// @@ -391,6 +398,64 @@ public void LogBuildFinished(bool success) WaitForLoggingToProcessEvents(); } + /// + /// In a single pass over the registered loggers, emits a message listing the enabled logger + /// type names and a describing each logger (including + /// any output file paths for implementations). + /// + private void LogAndRegisterLoggers() + { + List listOfLoggers = new(); + var loggerDescriptions = new List(); + + foreach (ILogger logger in Loggers) + { + ILogger actualLogger = UnwrapLogger(logger); + Type loggerType = actualLogger.GetType(); + + listOfLoggers.Add(loggerType.FullName ?? loggerType.Name); + + var outputFilePaths = new List(); + if (actualLogger is IFileOutputLogger fileLogger) + { + foreach (string outputFilePath in fileLogger.OutputFilePaths) + { + if (!string.IsNullOrEmpty(outputFilePath)) + { + outputFilePaths.Add(outputFilePath); + } + } + } + + loggerDescriptions.Add(new RegisteredLoggerInfo( + loggerName: loggerType.Name, + outputFilePaths: outputFilePaths.Count > 0 ? outputFilePaths : null, + verbosity: actualLogger.Verbosity, + loggerTypeFullName: loggerType.FullName, + parameters: actualLogger.Parameters)); + } + + if (listOfLoggers.Count != 0) + { + var msgEvent = new BuildMessageEventArgs( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LogEnabledLogs", string.Join(", ", listOfLoggers)), + null, null, MessageImportance.Low); + msgEvent.BuildEventContext = BuildEventContext.Invalid; + ProcessLoggingEvent(msgEvent); + } + + if (loggerDescriptions.Count > 0) + { + var registerEvent = new LoggersRegisteredEventArgs(loggerDescriptions); + registerEvent.BuildEventContext = BuildEventContext.Invalid; + ProcessLoggingEvent(registerEvent); + } + } + private ILogger UnwrapLogger(ILogger logger) + { + return logger is ReusableLogger reusable ? reusable.OriginalLogger : logger; + } + /// public void LogBuildCanceled() { diff --git a/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs b/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs index d87b80c363f..969cb0f400a 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Build.Logging @@ -45,5 +45,6 @@ public enum BinaryLogRecordKind BuildCheckAcquisition, BuildSubmissionStarted, BuildCanceled, + LoggersRegistered, } } diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index 73031a02798..ffabf948e86 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -48,7 +48,7 @@ public sealed class BinaryLoggerParameters /// text logs that erase a lot of useful information. /// /// The logger is public so that it can be instantiated from MSBuild.exe via command-line switch. - public sealed class BinaryLogger : ILogger + public sealed class BinaryLogger : ILogger, IFileOutputLogger { // version 2: // - new BuildEventContext.EvaluationId @@ -109,6 +109,8 @@ public sealed class BinaryLogger : ILogger // - new record kind: BuildCanceledEventArgs // version 25: // - add extra information to PropertyInitialValueSetEventArgs and PropertyReassignmentEventArgs and change message formatting logic. + // version 26: + // - new record kind: LoggersRegisteredEventArgs (reports registered loggers and their output file paths) // MAKE SURE YOU KEEP BuildEventArgsWriter AND StructuredLogViewer.BuildEventArgsWriter IN SYNC WITH THE CHANGES ABOVE. // Both components must stay in sync to avoid issues with logging or event handling in the products. @@ -119,7 +121,7 @@ public sealed class BinaryLogger : ILogger // The current version of the binary log representation. // Changes with each update of the binary log format. - internal const int FileFormatVersion = 25; + internal const int FileFormatVersion = 26; // The minimum version of the binary log reader that can read log of above version. // This should be changed only when the binary log format is changed in a way that would prevent it from being @@ -297,6 +299,12 @@ private static bool TryParsePathParameter(string parameter, out string filePath) internal string FilePath { get; private set; } + /// + System.Collections.Generic.IReadOnlyList IFileOutputLogger.OutputFilePaths + => AdditionalFilePaths is null || AdditionalFilePaths.Count == 0 + ? [FilePath] + : [FilePath, .. AdditionalFilePaths]; + /// /// Gets or sets additional output file paths. When set, the binlog will be copied to all these paths /// after the build completes. The primary FilePath will be used as the temporary write location. diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs index 69afeee1674..ece6c23e610 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs @@ -330,6 +330,7 @@ void HandleError(FormatErrorMessage msgFactory, bool noThrow, ReaderErrorType re BinaryLogRecordKind.BuildCheckTracing => ReadBuildCheckTracingEventArgs(), BinaryLogRecordKind.BuildCheckAcquisition => ReadBuildCheckAcquisitionEventArgs(), BinaryLogRecordKind.BuildCanceled => ReadBuildCanceledEventArgs(), + BinaryLogRecordKind.LoggersRegistered => ReadLoggersRegisteredEventArgs(), _ => null }; @@ -1287,6 +1288,39 @@ private BuildEventArgs ReadBuildCanceledEventArgs() return e; } + private BuildEventArgs ReadLoggersRegisteredEventArgs() + { + var fields = ReadBuildEventArgsFields(); + int count = ReadInt32(); + var loggers = new List(count); + for (int i = 0; i < count; i++) + { + string loggerName = ReadDeduplicatedString()!; + string loggerTypeFullName = ReadDeduplicatedString()!; + string parameters = ReadDeduplicatedString()!; + + LoggerVerbosity? verbosity = null; + if (ReadBoolean()) + { + verbosity = (LoggerVerbosity)ReadInt32(); + } + + int pathCount = ReadInt32(); + var outputFilePaths = new string[pathCount]; + for (int j = 0; j < pathCount; j++) + { + outputFilePaths[j] = ReadDeduplicatedString()!; + } + + loggers.Add(new RegisteredLoggerInfo(loggerName, outputFilePaths, verbosity, loggerTypeFullName, parameters)); + } + + var e = new LoggersRegisteredEventArgs(loggers); + SetCommonFields(e, fields); + + return e; + } + /// /// For errors and warnings these 8 fields are written out explicitly /// (their presence is not marked as a bit in the flags). So we have to diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs index f1a1b6d47e1..907fe74e711 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs @@ -221,6 +221,7 @@ private BinaryLogRecordKind WriteCore(BuildEventArgs e) case ProjectEvaluationFinishedEventArgs projectEvaluationFinished: return Write(projectEvaluationFinished); case BuildCheckTracingEventArgs buildCheckTracing: return Write(buildCheckTracing); case BuildCheckAcquisitionEventArgs buildCheckAcquisition: return Write(buildCheckAcquisition); + case LoggersRegisteredEventArgs loggersRegistered: return Write(loggersRegistered); default: // convert all unrecognized objects to message // and just preserve the message @@ -316,6 +317,31 @@ private BinaryLogRecordKind Write(BuildCanceledEventArgs e) return BinaryLogRecordKind.BuildCanceled; } + private BinaryLogRecordKind Write(LoggersRegisteredEventArgs e) + { + WriteBuildEventArgsFields(e); + Write(e.Loggers.Count); + foreach (var logger in e.Loggers) + { + WriteDeduplicatedString(logger.LoggerName); + WriteDeduplicatedString(logger.LoggerTypeFullName); + WriteDeduplicatedString(logger.Parameters); + Write(logger.Verbosity.HasValue); + if (logger.Verbosity.HasValue) + { + Write((int)logger.Verbosity.Value); + } + + Write(logger.OutputFilePaths.Count); + foreach (var path in logger.OutputFilePaths) + { + WriteDeduplicatedString(path); + } + } + + return BinaryLogRecordKind.LoggersRegistered; + } + private BinaryLogRecordKind Write(ProjectEvaluationStartedEventArgs e) { WriteBuildEventArgsFields(e, writeMessage: false); diff --git a/src/Build/Logging/FileLogger.cs b/src/Build/Logging/FileLogger.cs index a2d306a1341..e7763fb43cf 100644 --- a/src/Build/Logging/FileLogger.cs +++ b/src/Build/Logging/FileLogger.cs @@ -25,10 +25,9 @@ namespace Microsoft.Build.Logging /// 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. /// - public class FileLogger : ConsoleLogger + public class FileLogger : ConsoleLogger, IFileOutputLogger { #region Constructors - /// /// Default constructor. /// @@ -239,6 +238,14 @@ private void ApplyFileLoggerParameter(string parameterName, string parameterValu /// private string _logFileName = "msbuild.log"; + /// + /// The path to the log file. + /// + internal string FilePath => Path.GetFullPath(_logFileName); + + /// + System.Collections.Generic.IReadOnlyList IFileOutputLogger.OutputFilePaths => new[] { FilePath }; + /// /// fileWriter is the stream that has been opened on our log file. /// diff --git a/src/Build/Logging/IFileOutputLogger.cs b/src/Build/Logging/IFileOutputLogger.cs new file mode 100644 index 00000000000..2a2c23241b5 --- /dev/null +++ b/src/Build/Logging/IFileOutputLogger.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Logging +{ + /// + /// Implemented by loggers that write the build log to one or more files on disk + /// (for example and ). + /// + /// + /// This is used solely to surface the log file paths in the build summary printed + /// by the console logger at the end of a build so that users can easily locate the + /// log files that were produced. It is not intended to represent project build + /// outputs (e.g. produced assemblies) or any other artifacts unrelated to logging. + /// + internal interface IFileOutputLogger + { + /// + /// Gets the absolute paths of the log files that this logger writes to. + /// Reported in the end-of-build summary emitted by the console logger. + /// + System.Collections.Generic.IReadOnlyList OutputFilePaths { get; } + } +} diff --git a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index bbc114d14d2..7e455bfafba 100644 --- a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs +++ b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -28,7 +28,7 @@ internal class ParallelConsoleLogger : BaseConsoleLogger /// Associate a (nodeID and project_context_id) to a target framework. /// internal Dictionary<(int nodeId, int contextId), string> propertyOutputMap = new Dictionary<(int nodeId, int contextId), string>(); - + private readonly List _registeredLoggers = new(); #region Constructors /// /// Default constructor. @@ -204,6 +204,7 @@ internal override void ResetConsoleLoggerState() _hasBuildStarted = false; // Reset the data structures created when the logger was created + _registeredLoggers.Clear(); propertyOutputMap = new Dictionary<(int, int), string>(); _buildEventManager = new BuildEventManager(); _deferredMessages = new Dictionary>(s_compareContextNodeId); @@ -288,6 +289,23 @@ public override void BuildFinishedHandler(object sender, BuildFinishedEventArgs resetColor(); } + // Show paths to the files created by enabled loggers. + if (ShowSummary == true + && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8) + && _registeredLoggers.Any(logger => logger.OutputFilePaths.Count > 0)) + { + WriteNewLine(); + + foreach (var logger in _registeredLoggers.Where(logger => logger.OutputFilePaths.Count > 0)) + { + string displayPaths = string.Join( + CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", + logger.OutputFilePaths); + + WriteLinePretty(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LogFileOutputPath", logger.LoggerName, displayPaths)); + } + } + // The decision whether or not to show a summary at this verbosity // was made during initialization. We just do what we're told. if (ShowSummary == true) @@ -1124,7 +1142,6 @@ public override void MessageHandler(object sender, BuildMessageEventArgs e) { return; } - if (e.BuildEventContext == null && e is AssemblyLoadBuildEventArgs) { return; @@ -1213,6 +1230,10 @@ public override void StatusEventHandler(object sender, BuildStatusEventArgs e) propertyOutputMap[evaluationKey] = value; } } + else if (e is LoggersRegisteredEventArgs loggerEvent) + { + _registeredLoggers.AddRange(loggerEvent.Loggers); + } else if (e is BuildCanceledEventArgs buildCanceled) { Console.WriteLine(e.Message); diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index 950bafb7025..003590435e9 100644 --- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs +++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs @@ -221,6 +221,12 @@ public EvalContext(BuildEventContext context) /// private bool _showNodesDisplay = true; + /// + /// Stores the registered loggers. + /// + private readonly List _registeredLoggers = new(); + + private uint? _originalConsoleMode; /// @@ -631,6 +637,21 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) if (_showSummary == true) { RenderBuildSummary(); + + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8) + && _registeredLoggers.Any(logger => logger.OutputFilePaths.Count > 0)) + { + Terminal.WriteLine(string.Empty); + + foreach (var logger in _registeredLoggers.Where(logger => logger.OutputFilePaths.Count > 0)) + { + string displayPaths = string.Join( + CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", + logger.OutputFilePaths.Select(outputPath => $"{AnsiCodes.LinkPrefix}{new Uri(outputPath).AbsoluteUri}{AnsiCodes.LinkInfix}{outputPath}{AnsiCodes.LinkSuffix}")); + + Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LogFileOutputPath", logger.LoggerName, displayPaths)); + } + } } if (_restoreFailed) @@ -659,6 +680,7 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) _projects.Clear(); _testRunSummaries.Clear(); + _registeredLoggers.Clear(); _buildErrorsCount = 0; _buildWarningsCount = 0; _restoreFailed = false; @@ -705,6 +727,9 @@ private void StatusEventRaised(object sender, BuildStatusEventArgs e) case ProjectEvaluationFinishedEventArgs evalFinish: CaptureEvalContext(evalFinish); break; + case LoggersRegisteredEventArgs loggerEvent: + _registeredLoggers.AddRange(loggerEvent.Loggers); + break; } } @@ -1180,7 +1205,6 @@ private void MessageRaised(object sender, BuildMessageEventArgs e) { return; } - string? message = e.Message; if (message is not null && e.Importance == MessageImportance.High) diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 1162535689a..b5878f190cc 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -575,6 +575,7 @@ + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index b63e28e1a7f..a58c19b174b 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -170,6 +170,14 @@ Build succeeded. + + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + + + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + Build started {0}. diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index d477592535b..d03abf1d95c 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -590,6 +590,16 @@ Načítá se následující modul plug-in mezipaměti projektu: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Podrobnost protokolování je nastavená na: {0}. diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index b077163c95f..fce6d091164 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -590,6 +590,16 @@ Folgendes Projektcache-Plug-In wird geladen: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Die Ausführlichkeit der Protokollierung ist auf "{0}" festgelegt. diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index 55684896818..5357cdf9ba4 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -590,6 +590,16 @@ Cargando el complemento de caché de proyectos siguiente:{0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. El nivel de detalle de registro está establecido en {0}. diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 5a75745c25b..7d5eba2fe40 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -590,6 +590,16 @@ Chargement du plug-in de cache de projet suivant :{0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. La verbosité de la journalisation a la valeur {0}. diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 2b5d85c0ef0..34a96bc8e0f 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -590,6 +590,16 @@ Caricamento del plug-in della cache del progetto seguente: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Il livello di dettaglio della registrazione è impostato su: {0}. diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index f493adcfa47..4222733447a 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -590,6 +590,16 @@ 次のプロジェクト キャッシュ プラグインを読み込んでいます: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. ログの詳細度は次のように設定されています: {0}。 diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index 0196db61439..3b7eef13039 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -590,6 +590,16 @@ 다음 프로젝트 캐시 플러그 인을 로드하는 중:{0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. 로깅의 세부 정보 표시가 {0}(으)로 설정되었습니다. diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 62a054e5ee6..2afe81e57d8 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -590,6 +590,16 @@ Ładowanie następującej wtyczki pamięci podręcznej projektu: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Szczegółowość rejestrowania została ustawiona na: {0}. diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index e5cc39449cf..973136431d1 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -590,6 +590,16 @@ Carregando o seguinte plug-in de cache do projeto: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. O detalhamento do log está definido como: {0}. diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 656a412dfee..16132547950 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -590,6 +590,16 @@ Идет загрузка следующего подключаемого модуля кэша проектов: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Уровень детализации журнала: {0}. diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index b7a6b30f8ec..94c3a71bf77 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -590,6 +590,16 @@ Şu proje önbelleği eklentisi yükleniyor:{0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. Günlük kaydı ayrıntı düzeyi {0} olarak ayarlandı. diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index 58c01e2ed66..b973ae7d242 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -590,6 +590,16 @@ 正在加载以下项目缓存插件: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. 日志记录详细程度设置为: {0}。 diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index 519dfb332d7..828db133257 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -590,6 +590,16 @@ 載入下列專案快取外掛程式: {0} + + Enabled logs: {0} + Enabled logs: {0} + {0} is a comma-separated list of log types (e.g. "Binary log", "File log"). + + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a comma-separated list of full file paths. + Logging verbosity is set to: {0}. 記錄詳細程度設定為: {0}。 diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs index 52ab8929eeb..301873a3182 100644 --- a/src/Framework/ChangeWaves.cs +++ b/src/Framework/ChangeWaves.cs @@ -35,7 +35,8 @@ internal static class ChangeWaves internal static readonly Version Wave18_5 = new Version(18, 5); internal static readonly Version Wave18_6 = new Version(18, 6); internal static readonly Version Wave18_7 = new Version(18, 7); - internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7]; + internal static readonly Version Wave18_8 = new Version(18, 8); + internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8]; /// /// Special value indicating that all features behind all Change Waves should be enabled. diff --git a/src/Framework/LoggersRegisteredEventArgs.cs b/src/Framework/LoggersRegisteredEventArgs.cs new file mode 100644 index 00000000000..9f346b550ec --- /dev/null +++ b/src/Framework/LoggersRegisteredEventArgs.cs @@ -0,0 +1,156 @@ +// 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.Generic; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Framework +{ + /// + /// Describes a single registered logger. + /// + [Serializable] + public sealed class RegisteredLoggerInfo + { + /// + /// Initialize a new instance of the RegisteredLoggerInfo class. + /// + public RegisteredLoggerInfo(string loggerName, IReadOnlyList? outputFilePaths = null, LoggerVerbosity? verbosity = null, string? loggerTypeFullName = null, string? parameters = null) + { + LoggerName = loggerName; + OutputFilePaths = outputFilePaths ?? Array.Empty(); + Verbosity = verbosity; + LoggerTypeFullName = loggerTypeFullName; + Parameters = parameters; + } + + /// + /// The name of the logger. + /// + public string LoggerName { get; } + + /// + /// The full type name of the logger. + /// + public string? LoggerTypeFullName { get; } + + /// + /// The logger parameters. + /// + public string? Parameters { get; } + + /// + /// The output file paths for the logger. + /// + public IReadOnlyList OutputFilePaths { get; } + + /// + /// The verbosity level of the logger. + /// + public LoggerVerbosity? Verbosity { get; } + } + + /// + /// Arguments for the loggers registered event, containing one or more logger registrations. + /// + [Serializable] + public sealed class LoggersRegisteredEventArgs : BuildStatusEventArgs + { + internal LoggersRegisteredEventArgs() + { + } + + /// + /// Initialize a new instance of the LoggersRegisteredEventArgs class. + /// + /// The list of registered loggers. + public LoggersRegisteredEventArgs(IReadOnlyList loggers) + : base(FormatMessage(loggers), null, null) + { + Loggers = loggers; + } + + /// + /// Formats a summary message listing loggers with output paths. + /// This serves as a fallback message for consumers that do not handle this event; + /// the console/terminal loggers format their own localized output from . + /// + private static string? FormatMessage(IReadOnlyList loggers) + { + var withPaths = loggers.Where(l => l.OutputFilePaths.Count > 0).ToList(); + if (withPaths.Count == 0) + { + return string.Empty; + } + + return string.Join("; ", withPaths.Select(l => string.Format( + CultureInfo.CurrentCulture, + "{0} wrote to: {1}", + l.LoggerName, + string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", l.OutputFilePaths)))); + } + + /// + /// The registered loggers. + /// + public IReadOnlyList Loggers { get; internal set; } = Array.Empty(); + + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + + writer.Write(Loggers.Count); + foreach (var logger in Loggers) + { + writer.Write(logger.LoggerName); + writer.WriteOptionalString(logger.LoggerTypeFullName); + writer.WriteOptionalString(logger.Parameters); + writer.Write(logger.Verbosity.HasValue); + if (logger.Verbosity.HasValue) + { + writer.Write((int)logger.Verbosity.Value); + } + + writer.Write(logger.OutputFilePaths.Count); + foreach (var path in logger.OutputFilePaths) + { + writer.Write(path); + } + } + } + + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + + int count = reader.ReadInt32(); + var loggers = new List(count); + for (int i = 0; i < count; i++) + { + string loggerName = reader.ReadString(); + string? loggerTypeFullName = reader.ReadOptionalString(); + string? parameters = reader.ReadOptionalString(); + + LoggerVerbosity? verbosity = null; + if (reader.ReadBoolean()) + { + verbosity = (LoggerVerbosity)reader.ReadInt32(); + } + + int pathCount = reader.ReadInt32(); + var outputFilePaths = new string[pathCount]; + for (int j = 0; j < pathCount; j++) + { + outputFilePaths[j] = reader.ReadString(); + } + + loggers.Add(new RegisteredLoggerInfo(loggerName, outputFilePaths, verbosity, loggerTypeFullName, parameters)); + } + + Loggers = loggers; + } + } +} diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 17589c57473..3b7efde1b02 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -243,6 +243,11 @@ internal enum LoggingEventType : int /// Event is /// WorkerNodeTelemetryEvent = 42, + + /// + /// Event is + /// + LoggersRegisteredEvent = 43, } #endregion @@ -543,6 +548,7 @@ private BuildEventArgs GetBuildEventArgFromId() LoggingEventType.BuildSubmissionStartedEvent => new BuildSubmissionStartedEventArgs(), LoggingEventType.BuildCanceledEvent => new BuildCanceledEventArgs("Build canceled."), LoggingEventType.WorkerNodeTelemetryEvent => new WorkerNodeTelemetryEventArgs(), + LoggingEventType.LoggersRegisteredEvent => new LoggersRegisteredEventArgs(), _ => throw new InternalErrorException("Should not get to the default of GetBuildEventArgFromId ID: " + _eventType) }; @@ -690,6 +696,10 @@ private LoggingEventType GetLoggingEventId(BuildEventArgs eventArg) { return LoggingEventType.WorkerNodeTelemetryEvent; } + else if (eventType == typeof(LoggersRegisteredEventArgs)) + { + return LoggingEventType.LoggersRegisteredEvent; + } else if (eventType == typeof(TargetStartedEventArgs)) { return LoggingEventType.TargetStartedEvent; diff --git a/src/Utilities.UnitTests/MuxLogger_Tests.cs b/src/Utilities.UnitTests/MuxLogger_Tests.cs index b4228b11909..1e75d9fec93 100644 --- a/src/Utilities.UnitTests/MuxLogger_Tests.cs +++ b/src/Utilities.UnitTests/MuxLogger_Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text.RegularExpressions; using System.Threading; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -126,7 +127,13 @@ public void BuildWithMuxLoggerEquivalentToNormalLogger() // This test was changed to not compare new lines because of https://github.com/dotnet/msbuild/issues/10493 // It will need to be changed once we fix the root cause of the issue - mockLogger.FullLog.Replace(Environment.NewLine, "").ShouldBe(mockLogger2.FullLog.Replace(Environment.NewLine, "")); + // Strip "Enabled logs: ..." because the direct logger is registered before BeginBuild and receives all + // BuildStarted-time events. The MuxLogger itself is also registered before BeginBuild, but its per-submission + // sub-logger is added only after PendBuildRequest returns a submission ID. By then the global BuildStarted + // event has already been processed, so the MuxLogger replays BuildStarted to the sub-logger but cannot replay + // follow-up messages emitted during BuildStarted processing. + string StripEnabledLogs(string log) => Regex.Replace(log.Replace(Environment.NewLine, ""), @"Enabled logs: .+?(?=Project |$)", ""); + StripEnabledLogs(mockLogger.FullLog).ShouldBe(StripEnabledLogs(mockLogger2.FullLog)); } ///