diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 2ca32f08433..a6fd5b23f0f 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -31,6 +31,7 @@ Change wave checks around features will be removed in the release that accompani ### 18.8 - [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319) +- [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 - [Copy task retries on ERROR_ACCESS_DENIED on non-Windows platforms to handle transient lock conflicts (e.g. macOS CoW filesystems)](https://github.com/dotnet/msbuild/issues/13463) diff --git a/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs b/src/Build.UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs index 3fe5fcd8e6e..32f5e89dcbf 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,52 @@ 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 LogBuildStartedLogsLoggerNames() + { + 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.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 +1842,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 +1909,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..a2ed7cbebff 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,21 @@ 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"), + new RegisteredLoggerInfo( + "ConsoleLogger", + outputFilePaths: null, + verbosity: LoggerVerbosity.Detailed, + parameters: "ShowTimestamp;ShowEventId"), + new RegisteredLoggerInfo( + "MultiFileLogger", + outputFilePaths: new[] { @"C:\logs\a.log", @"C:\logs\b.log" }, + verbosity: LoggerVerbosity.Diagnostic, + parameters: "LogFile=a.log;LogFile=b.log"), + }), }; foreach (BuildEventArgs arg in testArgs) { diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index 5848692f084..1867f40879e 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -277,10 +277,13 @@ public void GlobalProperties_ForwardedToAutoEjectedTaskInMultiThreadedMode() public void GlobalProperties_UseBuildLevelWhenChangeWaveDisabled() { using TestEnvironment env = TestEnvironment.Create(_output); - env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_6.ToString()); - string testDir = env.CreateFolder().Path; + try + { + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_6.ToString()); + string testDir = env.CreateFolder().Path; - string projectContents = $@" + string projectContents = $@" @@ -290,32 +293,37 @@ public void GlobalProperties_UseBuildLevelWhenChangeWaveDisabled() "; - string projectFile = Path.Combine(testDir, "Test.proj"); - File.WriteAllText(projectFile, projectContents); - - // These request-level properties should NOT be forwarded when the wave is disabled - var requestGlobalProperties = new Dictionary - { - ["TestRequestProperty"] = "RequestValue", - }; + string projectFile = Path.Combine(testDir, "Test.proj"); + File.WriteAllText(projectFile, projectContents); - var logger = new MockLogger(_output); - BuildResult buildResult = BuildManager.DefaultBuildManager.Build( - new BuildParameters + // These request-level properties should NOT be forwarded when the wave is disabled + var requestGlobalProperties = new Dictionary { - MultiThreaded = true, - MaxNodeCount = 4, - Loggers = [logger], - EnableNodeReuse = false, - }, - new BuildRequestData(projectFile, requestGlobalProperties, null, ["Test"], null)); - - buildResult.OverallResult.ShouldBe(BuildResultCode.Success); - - // With wave disabled, build-level properties are used (empty in this test), - // so request-level properties should NOT appear - logger.FullLog.ShouldNotContain("GlobalProperty: TestRequestProperty=RequestValue"); - logger.FullLog.ShouldContain("GlobalPropertyCount = 0"); + ["TestRequestProperty"] = "RequestValue", + }; + + var logger = new MockLogger(_output); + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters + { + MultiThreaded = true, + MaxNodeCount = 4, + Loggers = [logger], + EnableNodeReuse = false, + }, + new BuildRequestData(projectFile, requestGlobalProperties, null, ["Test"], null)); + + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // With wave disabled, build-level properties are used (empty in this test), + // so request-level properties should NOT appear + logger.FullLog.ShouldNotContain("GlobalProperty: TestRequestProperty=RequestValue"); + logger.FullLog.ShouldContain("GlobalPropertyCount = 0"); + } + finally + { + ChangeWaves.ResetStateForTests(); + } } /// diff --git a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs index 12d33ccc514..7caba415370 100644 --- a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs +++ b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs @@ -109,6 +109,33 @@ public void RoundtripBuildCanceledEventArgs() e => e.Timestamp.ToString()); } + [Fact] + public void RoundtripLoggersRegisteredEventArgs() + { + var args = new LoggersRegisteredEventArgs(new List + { + new RegisteredLoggerInfo("FileLogger", new[] { @"C:\logs\build.log" }), + new RegisteredLoggerInfo("BinaryLogger"), + new RegisteredLoggerInfo( + "ConsoleLogger", + outputFilePaths: null, + verbosity: LoggerVerbosity.Detailed, + parameters: "ShowTimestamp;ShowEventId"), + new RegisteredLoggerInfo( + "MultiFileLogger", + outputFilePaths: new[] { @"C:\logs\a.log", @"C:\logs\b.log" }, + verbosity: LoggerVerbosity.Diagnostic, + parameters: "LogFile=a.log;LogFile=b.log"), + }); + + Roundtrip(args, + e => e.Loggers.Count.ToString(CultureInfo.InvariantCulture), + e => string.Join("|", e.Loggers.Select(l => l.LoggerName)), + e => string.Join("|", e.Loggers.Select(l => l.Parameters ?? "")), + e => string.Join("|", e.Loggers.Select(l => l.Verbosity?.ToString() ?? "")), + e => string.Join("|", e.Loggers.Select(l => string.Join(",", l.OutputFilePaths)))); + } + [Fact] public void RoundtripBuildSubmissionStartedEventArgs() { diff --git a/src/Build.UnitTests/ConsoleLogger_Tests.cs b/src/Build.UnitTests/ConsoleLogger_Tests.cs index 1f04286b23b..50ad8ae640e 100644 --- a/src/Build.UnitTests/ConsoleLogger_Tests.cs +++ b/src/Build.UnitTests/ConsoleLogger_Tests.cs @@ -1961,6 +1961,84 @@ public void TestPrintTargetNamePerMessage() actualLog.ShouldContain("t:"); } + /// + /// + /// When the loggers print their output paths feature wave is enabled (default), + /// the ParallelConsoleLogger end-of-build summary should list each registered logger + /// that wrote to a file along with its output path(s). + /// + [Fact] + public void LogFileOutputPaths_PrintedInSummary_WhenWaveEnabled() + { + try + { + ChangeWaves.ResetStateForTests(); + + SimulatedConsole sc = new SimulatedConsole(); + ParallelConsoleLogger cl = new ParallelConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + EventSourceSink es = new EventSourceSink(); + cl.Initialize(es); + + BuildStartedEventArgs bsea = new BuildStartedEventArgs("bs", null); + cl.BuildStartedHandler(null, bsea); + + LoggersRegisteredEventArgs lrea = new LoggersRegisteredEventArgs(new List + { + new RegisteredLoggerInfo("FileLogger", new[] { @"C:\logs\build.log" }), + }); + cl.StatusEventHandler(null, lrea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + cl.BuildFinishedHandler(null, bfea); + + sc.ToString().ShouldContain("FileLogger"); + sc.ToString().ShouldContain(@"C:\logs\build.log"); + } + finally + { + ChangeWaves.ResetStateForTests(); + } + } + + /// + /// When the loggers print their output paths feature wave is disabled + /// via MSBUILDDISABLEFEATURESFROMVERSION, the ParallelConsoleLogger summary + /// must preserve the legacy behavior and NOT print logger output paths. + /// + [Fact] + public void LogFileOutputPaths_NotPrintedInSummary_WhenWaveDisabled() + { + using TestEnvironment env = TestEnvironment.Create(); + try + { + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_8.ToString()); + + SimulatedConsole sc = new SimulatedConsole(); + ParallelConsoleLogger cl = new ParallelConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + EventSourceSink es = new EventSourceSink(); + cl.Initialize(es); + + BuildStartedEventArgs bsea = new BuildStartedEventArgs("bs", null); + cl.BuildStartedHandler(null, bsea); + + LoggersRegisteredEventArgs lrea = new LoggersRegisteredEventArgs(new List + { + new RegisteredLoggerInfo("FileLogger", new[] { @"C:\logs\build.log" }), + }); + cl.StatusEventHandler(null, lrea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + cl.BuildFinishedHandler(null, bfea); + + sc.ToString().ShouldNotContain(@"C:\logs\build.log"); + } + finally + { + ChangeWaves.ResetStateForTests(); + } + } + /// /// 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 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 48f04b93e61..983a62d9293 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,15 @@ public void LogBuildStarted() // Make sure we process this event before going any further WaitForLoggingToProcessEvents(); + + // Register Loggers and print out all the enabled loggers. + // Gated behind ChangeWaves.Wave18_8 so that disabling the wave fully suppresses + // the new "Enabled loggers" message and the new LoggersRegisteredEventArgs, + // not just their rendering in the console/terminal loggers. + if (!OnlyLogCriticalEvents && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)) + { + LogAndRegisterLoggers(); + } } /// @@ -391,6 +401,63 @@ public void LogBuildFinished(bool success) WaitForLoggingToProcessEvents(); } + /// + /// 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.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, + 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..77d84cd94da 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; } + /// + 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..f07da38e10a 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,38 @@ 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 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, 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..6bd274e532a 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,30 @@ 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.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..95a7e52f5e0 --- /dev/null +++ b/src/Build/Logging/IFileOutputLogger.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; + +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. + /// + IReadOnlyList OutputFilePaths { get; } + } +} diff --git a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index fbeaefe7e15..f180de40fc2 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); @@ -322,6 +323,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(string.Format(CultureInfo.CurrentCulture, Microsoft.Build.Framework.Resources.SR.LogFileOutputPath, logger.LoggerName, displayPaths)); + } + } + // Show build time if verbosity is normal, detailed or diagnostic or the user specified to // show the summary. 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 02df04d569d..c4bf2af4aa4 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; /// @@ -626,6 +632,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(string.Format(CultureInfo.CurrentCulture, Microsoft.Build.Framework.Resources.SR.LogFileOutputPath, logger.LoggerName, displayPaths)); + } + } } if (_restoreFailed) @@ -654,6 +675,7 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) _projects.Clear(); _testRunSummaries.Clear(); + _registeredLoggers.Clear(); _buildErrorsCount = 0; _buildWarningsCount = 0; _restoreFailed = false; @@ -700,6 +722,9 @@ private void StatusEventRaised(object sender, BuildStatusEventArgs e) case ProjectEvaluationFinishedEventArgs evalFinish: CaptureEvalContext(evalFinish); break; + case LoggersRegisteredEventArgs loggerEvent: + _registeredLoggers.AddRange(loggerEvent.Loggers); + break; } } @@ -1175,7 +1200,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 7f3066e974b..67a7baa3634 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -574,6 +574,7 @@ + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 551b617d3ed..4dfca94e333 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -145,7 +145,7 @@ The operation cannot be completed because EndBuild has already been called but existing submissions have not yet completed. - + Property '{0}' with value '{1}' expanded from the environment. @@ -170,6 +170,10 @@ Build succeeded. + + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + Build started {0}. @@ -479,7 +483,7 @@ likely because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a special exception to abort the build. - + MSB3094: "{2}" refers to {0} item(s), and "{3}" refers to {1} item(s). They must have the same number of items. {StrBegin="MSB3094: "} @@ -604,7 +608,7 @@ LOCALIZATION: "{0}" is the expression that was bad. "{1}" is a message from an FX exception that describes why the expression is bad. - + Found multiple overloads for method "{0}" with {1} parameter(s). That is currently not supported. @@ -1175,7 +1179,7 @@ LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized - + MSB4181: The "{0}" task returned false but did not log an error. {StrBegin="MSB4181: "} @@ -1309,7 +1313,7 @@ MSB4067: The element <{0}> beneath element <{1}> is unrecognized. {StrBegin="MSB4067: "} - + MSB4067: The element <{0}> beneath element <{1}> is unrecognized. If you intended this to be a property, enclose it within a <PropertyGroup> element. {StrBegin="MSB4067: "} @@ -1778,7 +1782,7 @@ Utilization: {0} Average Utilization: {1:###.0} MSB4231: ProjectRootElement can't reload if it contains unsaved changes. {StrBegin="MSB4231: "} - + The parameters have been truncated beyond this point. To view all parameters, clear the MSBUILDTRUNCATETASKINPUTLOGGING environment variable. @@ -2019,9 +2023,9 @@ Utilization: {0} Average Utilization: {1:###.0} - {0} -> Cache Hit + {0} -> Cache Hit - {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. + {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. @@ -2090,7 +2094,7 @@ Utilization: {0} Average Utilization: {1:###.0} Imported files archive exceeded 2GB limit and it's not embedded. - Forward compatible reading is not supported for file format version {0} (needs >= 18). + Forward compatible reading is not supported for file format version {0} (needs >= 18). LOCALIZATION: {0} is an integer number denoting version. @@ -2213,19 +2217,19 @@ Utilization: {0} Average Utilization: {1:###.0} It is recommended to specify explicit 'Culture' metadata, or 'WithCulture=false' metadata with 'EmbeddedResource' item in order to avoid wrong or nondeterministic culture estimation. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies 'EmbeddedResource' item '{1}', that has possibly a culture denoting extension ('{2}'), but explicit 'Culture' nor 'WithCulture=false' metadata are not specified. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Avoid specifying 'Always' for 'CopyToOutputDirectory' as this can lead to unnecessary copy operations during build. Use 'PreserveNewest' or 'IfDifferent' metadata value, or set the 'SkipUnchangedFilesOnCopyAlways' property to true to employ more effective copying. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies '{1}' item '{2}', that has 'CopyToOutputDirectory' set as 'Always'. Change the metadata or use 'CopyToOutputDirectory' property. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. 'TargetFramework' (singular) and 'TargetFrameworks' (plural) properties should not be specified in the scripts at the same time. @@ -2486,11 +2490,4 @@ Utilization: {0} Average Utilization: {1:###.0} MSB4280: The environment variable DOTNET_HOST_PATH is set to a directory ("{0}") instead of a path to the dotnet executable. This can lead to build errors in tasks that use this variable such as the Roslyn compiler. Either unset the variable or update it to point to the dotnet executable directly (e.g. "C:\Program Files\dotnet\dotnet.exe"). {StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it. - diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 6be75946e85..156fdfdf4b7 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -590,6 +590,11 @@ Načítá se následující modul plug-in mezipaměti projektu: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 6ddc83aa214..3f8b5c2fab2 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -590,6 +590,11 @@ Folgendes Projektcache-Plug-In wird geladen: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 3076fcb2451..bf66606b94f 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -590,6 +590,11 @@ Cargando el complemento de caché de proyectos siguiente:{0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 bff0a8d988b..74d755ccd0f 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -590,6 +590,11 @@ Chargement du plug-in de cache de projet suivant :{0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 942869c420a..7300be92fea 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -590,6 +590,11 @@ Caricamento del plug-in della cache del progetto seguente: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 7bdbc8f4d7a..e28db0f0a80 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -590,6 +590,11 @@ 次のプロジェクト キャッシュ プラグインを読み込んでいます: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 1cb2d90d641..3b699f4633b 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -590,6 +590,11 @@ 다음 프로젝트 캐시 플러그 인을 로드하는 중:{0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 be8119c6b9c..ab6addefc89 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -590,6 +590,11 @@ Ładowanie następującej wtyczki pamięci podręcznej projektu: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 8cc115856a6..7b0e9327274 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -590,6 +590,11 @@ Carregando o seguinte plug-in de cache do projeto: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 9563004fb21..a3537edf997 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -590,6 +590,11 @@ Идет загрузка следующего подключаемого модуля кэша проектов: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 052ba8f14a0..d64f48c37d4 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -590,6 +590,11 @@ Şu proje önbelleği eklentisi yükleniyor:{0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 68d1004f90c..3e8fe53f92c 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -590,6 +590,11 @@ 正在加载以下项目缓存插件: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + 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 43a985a0871..8caa22c8bb3 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -590,6 +590,11 @@ 載入下列專案快取外掛程式: {0} + + Enabled loggers: {0} + Enabled loggers: {0} + {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). + Logging verbosity is set to: {0}. 記錄詳細程度設定為: {0}。 diff --git a/src/Framework/LoggersRegisteredEventArgs.cs b/src/Framework/LoggersRegisteredEventArgs.cs new file mode 100644 index 00000000000..97c128ed5e8 --- /dev/null +++ b/src/Framework/LoggersRegisteredEventArgs.cs @@ -0,0 +1,148 @@ +// 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? parameters = null) + { + LoggerName = loggerName; + OutputFilePaths = outputFilePaths ?? Array.Empty(); + Verbosity = verbosity; + Parameters = parameters; + } + + /// + /// The name of the logger. + /// + public string LoggerName { 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, + SR.LogFileOutputPath, + l.LoggerName, + string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", l.OutputFilePaths)))); + } + + /// + /// The registered loggers. + /// + public IReadOnlyList Loggers { get; private 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.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? 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, parameters)); + } + + Loggers = loggers; + } + } +} diff --git a/src/Framework/Resources/SR.resx b/src/Framework/Resources/SR.resx index ebcc88a74d5..3ffac876523 100644 --- a/src/Framework/Resources/SR.resx +++ b/src/Framework/Resources/SR.resx @@ -162,6 +162,10 @@ The parameter '{0}' can only be a file name and cannot include a directory. + + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + The path "{0}" used for debug logs is too long. Set it to a shorter value using the MSBUILDDEBUGPATH environment variable or change your system configuration to allow long paths. @@ -199,4 +203,4 @@ '{0}' contains no elements. - \ No newline at end of file + diff --git a/src/Framework/Resources/xlf/SR.cs.xlf b/src/Framework/Resources/xlf/SR.cs.xlf index 32d075daa4f..f638a77a50c 100644 --- a/src/Framework/Resources/xlf/SR.cs.xlf +++ b/src/Framework/Resources/xlf/SR.cs.xlf @@ -102,6 +102,11 @@ Parametr {0} může být jenom název souboru a nemůže obsahovat adresář. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Cesta musí začínat kořenem. diff --git a/src/Framework/Resources/xlf/SR.de.xlf b/src/Framework/Resources/xlf/SR.de.xlf index 6b53f58a3fa..ae4edd05a9b 100644 --- a/src/Framework/Resources/xlf/SR.de.xlf +++ b/src/Framework/Resources/xlf/SR.de.xlf @@ -102,6 +102,11 @@ Der Parameter '{0}' kann nur ein Dateiname sein und darf kein Verzeichnis enthalten. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Der Pfad muss einen Stamm besitzen. diff --git a/src/Framework/Resources/xlf/SR.es.xlf b/src/Framework/Resources/xlf/SR.es.xlf index daf26188dba..4a89306b8e6 100644 --- a/src/Framework/Resources/xlf/SR.es.xlf +++ b/src/Framework/Resources/xlf/SR.es.xlf @@ -102,6 +102,11 @@ El parámetro "{0}" solo puede ser el nombre de un archivo y no puede incluir un directorio. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Debe ser una ruta de acceso raíz. diff --git a/src/Framework/Resources/xlf/SR.fr.xlf b/src/Framework/Resources/xlf/SR.fr.xlf index 90d5bf48fa1..04407b8a196 100644 --- a/src/Framework/Resources/xlf/SR.fr.xlf +++ b/src/Framework/Resources/xlf/SR.fr.xlf @@ -102,6 +102,11 @@ Le paramètre "{0}" peut uniquement être un nom de fichier et ne peut pas inclure de répertoire. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Le chemin doit être associé à une racine. diff --git a/src/Framework/Resources/xlf/SR.it.xlf b/src/Framework/Resources/xlf/SR.it.xlf index 9f24bafe1ca..6d74479592f 100644 --- a/src/Framework/Resources/xlf/SR.it.xlf +++ b/src/Framework/Resources/xlf/SR.it.xlf @@ -102,6 +102,11 @@ Il parametro '{0}' può solo essere un nome file e non può includere una directory. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Il percorso deve contenere una radice. diff --git a/src/Framework/Resources/xlf/SR.ja.xlf b/src/Framework/Resources/xlf/SR.ja.xlf index 4490b72a34e..bcd289e9cdf 100644 --- a/src/Framework/Resources/xlf/SR.ja.xlf +++ b/src/Framework/Resources/xlf/SR.ja.xlf @@ -102,6 +102,11 @@ パラメーター '{0}' に使用できるのはファイル名のみで、ディレクトリを含めることはできません。 + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. パスはルート指定パスである必要があります。 diff --git a/src/Framework/Resources/xlf/SR.ko.xlf b/src/Framework/Resources/xlf/SR.ko.xlf index d4df3b1c552..672af58685b 100644 --- a/src/Framework/Resources/xlf/SR.ko.xlf +++ b/src/Framework/Resources/xlf/SR.ko.xlf @@ -102,6 +102,11 @@ '{0}' 매개 변수는 파일 이름일 수만 있으며 디렉터리를 포함할 수 없습니다. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. 루트 경로로 지정해야 합니다. diff --git a/src/Framework/Resources/xlf/SR.pl.xlf b/src/Framework/Resources/xlf/SR.pl.xlf index 8a0af80daaf..48e6d87f315 100644 --- a/src/Framework/Resources/xlf/SR.pl.xlf +++ b/src/Framework/Resources/xlf/SR.pl.xlf @@ -102,6 +102,11 @@ Parametr „{0}” może zawierać tylko nazwę pliku i nie może zawierać katalogu. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Ścieżka musi zaczynać się od katalogu głównego. diff --git a/src/Framework/Resources/xlf/SR.pt-BR.xlf b/src/Framework/Resources/xlf/SR.pt-BR.xlf index 0cf2bf2f94b..b93272a86b1 100644 --- a/src/Framework/Resources/xlf/SR.pt-BR.xlf +++ b/src/Framework/Resources/xlf/SR.pt-BR.xlf @@ -102,6 +102,11 @@ O parâmetro '{0}' pode ser somente um nome de arquivo e não pode incluir um diretório. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Caminho deve ter raiz. diff --git a/src/Framework/Resources/xlf/SR.ru.xlf b/src/Framework/Resources/xlf/SR.ru.xlf index 19c782d0903..b938871c2b8 100644 --- a/src/Framework/Resources/xlf/SR.ru.xlf +++ b/src/Framework/Resources/xlf/SR.ru.xlf @@ -102,6 +102,11 @@ Параметр "{0}" может быть только именем файла и не может включать в себя каталог. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Путь должен иметь корень. diff --git a/src/Framework/Resources/xlf/SR.tr.xlf b/src/Framework/Resources/xlf/SR.tr.xlf index fd1ff3fa370..44aa7e417f6 100644 --- a/src/Framework/Resources/xlf/SR.tr.xlf +++ b/src/Framework/Resources/xlf/SR.tr.xlf @@ -102,6 +102,11 @@ '{0}' yalnızca bir dosya adı olabilir ve dizin içeremez. + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. Yol kökü belirtilmelidir. diff --git a/src/Framework/Resources/xlf/SR.zh-Hans.xlf b/src/Framework/Resources/xlf/SR.zh-Hans.xlf index 5dcdb7945bc..af7907f2b12 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hans.xlf @@ -102,6 +102,11 @@ 参数“{0}”只能是文件名,不能包含目录。 + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. 路径必须是根路径。 diff --git a/src/Framework/Resources/xlf/SR.zh-Hant.xlf b/src/Framework/Resources/xlf/SR.zh-Hant.xlf index f70b3542096..9e561450861 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hant.xlf @@ -102,6 +102,11 @@ 參數 '{0}' 只可以是檔案名稱,不得包含目錄。 + + {0} wrote to: {1} + {0} wrote to: {1} + {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). + Path must be rooted. 路徑必須為根路徑。 diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 4306a91986d..a53356706d4 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..5109f024cbb 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 loggers: ..." 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 StripEnabledLoggers(string log) => Regex.Replace(log.Replace(Environment.NewLine, ""), @"Enabled loggers: .+?(?=Project |$)", ""); + StripEnabledLoggers(mockLogger.FullLog).ShouldBe(StripEnabledLoggers(mockLogger2.FullLog)); } ///