|
// 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.IO;
using System.IO.Compression;
using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Telemetry;
using Microsoft.Build.Shared;
#nullable disable
namespace Microsoft.Build.Logging
{
/// <summary>
/// Represents the parsed parameters for a BinaryLogger.
/// </summary>
public sealed class BinaryLoggerParameters
{
/// <summary>
/// Gets the log file path. Returns null if not specified or if the path contains wildcards.
/// </summary>
public string LogFilePath { get; internal set; }
/// <summary>
/// Gets the project imports collection mode.
/// </summary>
public BinaryLogger.ProjectImportsCollectionMode ProjectImportsCollectionMode { get; internal set; } = BinaryLogger.ProjectImportsCollectionMode.Embed;
/// <summary>
/// Gets whether the ProjectImports parameter was explicitly specified in the parameters string.
/// </summary>
internal bool HasProjectImportsParameter { get; set; }
/// <summary>
/// Gets whether to omit initial info from the log.
/// </summary>
public bool OmitInitialInfo { get; internal set; }
}
/// <summary>
/// A logger that serializes all incoming BuildEventArgs in a compressed binary file (*.binlog). The file
/// can later be played back and piped into other loggers (file, console, etc) to reconstruct the log contents
/// as if a real build was happening. Additionally, this format can be read by tools for
/// analysis or visualization. Since the file format preserves structure, tools don't have to parse
/// text logs that erase a lot of useful information.
/// </summary>
/// <remarks>The logger is public so that it can be instantiated from MSBuild.exe via command-line switch.</remarks>
public sealed class BinaryLogger : ILogger
{
// version 2:
// - new BuildEventContext.EvaluationId
// - new record kinds: ProjectEvaluationStarted, ProjectEvaluationFinished
// version 3:
// - new ProjectImportedEventArgs.ImportIgnored
// version 4:
// - new TargetSkippedEventArgs
// - new TargetStartedEventArgs.BuildReason
// version 5:
// - new EvaluationFinished.ProfilerResult
// version 6:
// - Ids and parent ids for the evaluation locations
// version 7:
// - Include ProjectStartedEventArgs.GlobalProperties
// version 8:
// - This was used in a now-reverted change but is the same as 9.
// version 9:
// - new record kinds: EnvironmentVariableRead, PropertyReassignment, UninitializedPropertyRead
// version 10:
// - new record kinds:
// * String - deduplicate strings by hashing and write a string record before it's used
// * NameValueList - deduplicate arrays of name-value pairs such as properties, items and metadata
// in a separate record and refer to those records from regular records
// where a list used to be written in-place
// version 11:
// - new record kind: TaskParameterEventArgs
// version 12:
// - add GlobalProperties, Properties and Items on ProjectEvaluationFinished
// version 13:
// - don't log Message where it can be recovered
// - log arguments for LazyFormattedBuildEventArgs
// - TargetSkippedEventArgs: added OriginallySucceeded, Condition, EvaluatedCondition
// version 14:
// - TargetSkippedEventArgs: added SkipReason, OriginalBuildEventContext
// version 15:
// - new record kind: ResponseFileUsedEventArgs
// version 16:
// - AssemblyLoadBuildEventArgs
// version 17:
// - Added extended data for types implementing IExtendedBuildEventArgs
// version 18:
// - Making ProjectStartedEventArgs, ProjectEvaluationFinishedEventArgs, AssemblyLoadBuildEventArgs equal
// between de/serialization roundtrips.
// - Adding serialized events lengths - to support forward compatible reading
// version 19:
// - GeneratedFileUsedEventArgs exposed for brief period of time (so let's continue with 20)
// version 20:
// - TaskStartedEventArgs: Added TaskAssemblyLocation property
// version 21:
// - TaskParameterEventArgs: Added ParameterName and PropertyName properties
// version 22:
// - extend EnvironmentVariableRead with location where environment variable was used.
// version 23:
// - new record kinds: BuildCheckMessageEvent, BuildCheckWarningEvent, BuildCheckErrorEvent,
// BuildCheckTracingEvent, BuildCheckAcquisitionEvent, BuildSubmissionStartedEvent
// version 24:
// - new record kind: BuildCanceledEventArgs
// version 25:
// - add extra information to PropertyInitialValueSetEventArgs and PropertyReassignmentEventArgs and change message formatting logic.
// 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.
// This should be never changed.
// The minimum version of the binary log reader that can read log of above version.
internal const int ForwardCompatibilityMinimalVersion = 18;
// The current version of the binary log representation.
// Changes with each update of the binary log format.
internal const int FileFormatVersion = 25;
// 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
// read by older readers. (changing of the individual BuildEventArgs or adding new is fine - as reader can
// skip them if they are not known to it. Example of change requiring the increment would be the introduction of strings deduplication)
internal const int MinimumReaderVersion = 18;
// Parameter name constants
private const string LogFileParameterPrefix = "LogFile=";
private const string BinlogFileExtension = ".binlog";
private const string OmitInitialInfoParameter = "OmitInitialInfo";
private const string ProjectImportsNoneParameter = "ProjectImports=None";
private const string ProjectImportsEmbedParameter = "ProjectImports=Embed";
private const string ProjectImportsZipFileParameter = "ProjectImports=ZipFile";
private Stream stream;
private BinaryWriter binaryWriter;
private BuildEventArgsWriter eventArgsWriter;
private ProjectImportsCollector projectImportsCollector;
private bool _initialTargetOutputLogging;
private bool _initialLogImports;
private string _initialIsBinaryLoggerEnabled;
/// <summary>
/// Describes whether to collect the project files (including imported project files) used during the build.
/// If the project files are collected they can be embedded in the log file or as a separate zip archive.
/// </summary>
public enum ProjectImportsCollectionMode
{
/// <summary>
/// Don't collect any files during the build.
/// </summary>
None,
/// <summary>
/// Embed all project files directly in the log file.
/// </summary>
Embed,
/// <summary>
/// Create an external .ProjectImports.zip archive for the project files.
/// </summary>
ZipFile,
}
/// <summary>
/// Parses the parameters string for a BinaryLogger.
/// </summary>
/// <param name="parametersString">The parameters string to parse (e.g., "LogFile=msbuild.binlog;ProjectImports=None").</param>
/// <returns>A <see cref="BinaryLoggerParameters"/> object containing the parsed parameters.</returns>
/// <exception cref="LoggerException">Thrown when the parameters string contains invalid parameters.</exception>
/// <remarks>
/// This method parses the semicolon-delimited parameters string used by the BinaryLogger.
/// Supported parameters include:
/// - LogFile=<path> or just <path> (must end with .binlog): specifies the output file path
/// - ProjectImports=None|Embed|ZipFile: controls project imports collection
/// - OmitInitialInfo: omits initial build information
///
/// Wildcards ({}) in the LogFile path are NOT expanded by this method. The returned LogFilePath
/// will be null for wildcard patterns, and callers should handle expansion separately if needed.
/// </remarks>
public static BinaryLoggerParameters ParseParameters(string parametersString)
{
if (parametersString == null)
{
throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", ""));
}
var result = new BinaryLoggerParameters();
var parameters = parametersString.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
foreach (var parameter in parameters)
{
if (TryParseProjectImports(parameter, result))
{
continue;
}
if (string.Equals(parameter, OmitInitialInfoParameter, StringComparison.OrdinalIgnoreCase))
{
result.OmitInitialInfo = true;
continue;
}
if (TryParsePathParameter(parameter, out string filePath))
{
result.LogFilePath = filePath;
continue;
}
throw new LoggerException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("InvalidBinaryLoggerParameters", parameter));
}
return result;
}
/// <summary>
/// Attempts to parse a ProjectImports parameter.
/// </summary>
/// <param name="parameter">The parameter to parse.</param>
/// <param name="result">The BinaryLoggerParameters object to update.</param>
/// <returns>True if the parameter was a ProjectImports parameter; otherwise, false.</returns>
private static bool TryParseProjectImports(string parameter, BinaryLoggerParameters result)
{
return TrySetProjectImportsMode(parameter, ProjectImportsNoneParameter, ProjectImportsCollectionMode.None, result)
|| TrySetProjectImportsMode(parameter, ProjectImportsEmbedParameter, ProjectImportsCollectionMode.Embed, result)
|| TrySetProjectImportsMode(parameter, ProjectImportsZipFileParameter, ProjectImportsCollectionMode.ZipFile, result);
}
/// <summary>
/// Attempts to match and set a ProjectImports mode.
/// </summary>
/// <param name="parameter">The parameter to check.</param>
/// <param name="expectedParameter">The expected parameter string.</param>
/// <param name="mode">The mode to set if matched.</param>
/// <param name="result">The BinaryLoggerParameters object to update.</param>
/// <returns>True if the parameter matched; otherwise, false.</returns>
private static bool TrySetProjectImportsMode(string parameter, string expectedParameter, ProjectImportsCollectionMode mode, BinaryLoggerParameters result)
{
if (string.Equals(parameter, expectedParameter, StringComparison.OrdinalIgnoreCase))
{
result.ProjectImportsCollectionMode = mode;
result.HasProjectImportsParameter = true;
return true;
}
return false;
}
/// <summary>
/// Attempts to parse a file path parameter from a BinaryLogger parameter string.
/// </summary>
/// <param name="parameter">The parameter to parse.</param>
/// <param name="filePath">The parsed file path, or null if the parameter contains wildcards.</param>
/// <returns>True if the parameter is a valid file path parameter; otherwise, false.</returns>
/// <remarks>
/// This method recognizes file paths in the following formats:
/// - "LogFile=<path>"
/// - "<path>" (must end with .binlog)
///
/// If the path contains wildcards ({}), the method returns true but sets filePath to null,
/// as wildcard expansion requires runtime context.
/// </remarks>
private static bool TryParsePathParameter(string parameter, out string filePath)
{
bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase);
if (hasPathPrefix)
{
parameter = parameter.Substring(LogFileParameterPrefix.Length);
}
parameter = parameter.Trim('"');
bool isWildcard = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12) && parameter.Contains("{}");
bool hasProperExtension = parameter.EndsWith(BinlogFileExtension, StringComparison.OrdinalIgnoreCase);
filePath = parameter;
if (isWildcard)
{
// For wildcards, we return true to indicate this is a valid path parameter,
// but set filePath to null since we can't expand it without instance context
filePath = null;
return true;
}
return hasProperExtension;
}
/// <summary>
/// Gets or sets whether to capture and embed project and target source files used during the build.
/// </summary>
public ProjectImportsCollectionMode CollectProjectImports { get; set; } = ProjectImportsCollectionMode.Embed;
internal string FilePath { get; private set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This property is intended for internal use by MSBuild command-line processing.
/// It should not be set by external code or logger implementations.
/// Use multiple logger instances with different Parameters instead.
/// </remarks>
public IReadOnlyList<string> AdditionalFilePaths { get; init; }
/// <summary> Gets or sets the verbosity level.</summary>
/// <remarks>
/// The binary logger Verbosity is always maximum (Diagnostic). It tries to capture as much
/// information as possible.
/// </remarks>
public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Diagnostic;
/// <summary>
/// Gets or sets the parameters. The only supported parameter is the output log file path (for example, "msbuild.binlog").
/// </summary>
public string Parameters { get; set; }
/// <summary>
/// Optional expander of wildcard(s) within the LogFile path parameter of a binlog <see cref="Parameters"/>.
/// Wildcards can be used in the LogFile parameter in a form for curly brackets ('{}', '{[param]}').
/// Currently, the only supported wildcard is '{}', the optional parameters within the curly brackets
/// are not currently supported, however the string parameter to the <see cref="PathParameterExpander"/> func
/// is reserved for this purpose.
/// </summary>
internal Func<string, string> PathParameterExpander { private get; set; } = ExpandPathParameter;
/// <summary>
/// Initializes the logger by subscribing to events of the specified event source and embedded content source.
/// </summary>
public void Initialize(IEventSource eventSource)
{
_initialTargetOutputLogging = Traits.Instance.EnableTargetOutputLogging;
_initialLogImports = Traits.Instance.EscapeHatches.LogProjectImports;
_initialIsBinaryLoggerEnabled = Environment.GetEnvironmentVariable("MSBUILDBINARYLOGGERENABLED");
Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", "true");
Environment.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");
Environment.SetEnvironmentVariable("MSBUILDBINARYLOGGERENABLED", bool.TrueString);
Traits.Instance.EscapeHatches.LogProjectImports = true;
Traits.Instance.EnableTargetOutputLogging = true;
bool logPropertiesAndItemsAfterEvaluation = Traits.Instance.EscapeHatches.LogPropertiesAndItemsAfterEvaluation ?? true;
ProcessParameters(out bool omitInitialInfo);
var replayEventSource = eventSource as IBinaryLogReplaySource;
try
{
string logDirectory = null;
try
{
logDirectory = Path.GetDirectoryName(FilePath);
}
catch (Exception)
{
// Directory creation is best-effort; if finding its path fails don't create the directory
// and possibly let the FileStream constructor below report the failure
}
if (logDirectory != null)
{
Directory.CreateDirectory(logDirectory);
}
stream = new FileStream(FilePath, FileMode.Create);
if (CollectProjectImports != ProjectImportsCollectionMode.None && replayEventSource == null)
{
projectImportsCollector = new ProjectImportsCollector(FilePath, CollectProjectImports == ProjectImportsCollectionMode.ZipFile);
projectImportsCollector.FileIOExceptionEvent += EventSource_AnyEventRaised;
}
if (eventSource is IEventSource3 eventSource3)
{
eventSource3.IncludeEvaluationMetaprojects();
}
if (logPropertiesAndItemsAfterEvaluation && eventSource is IEventSource4 eventSource4)
{
eventSource4.IncludeEvaluationPropertiesAndItems();
}
}
catch (Exception e)
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidFileLoggerFile", FilePath, e.Message);
throw new LoggerException(message, e, errorCode, helpKeyword);
}
stream = new GZipStream(stream, CompressionLevel.Optimal);
// wrapping the GZipStream in a buffered stream significantly improves performance
// and the max throughput is reached with a 32K buffer. See details here:
// https://github.com/dotnet/runtime/issues/39233#issuecomment-745598847
stream = new BufferedStream(stream, bufferSize: 32768);
binaryWriter = new BinaryWriter(stream);
eventArgsWriter = new BuildEventArgsWriter(binaryWriter);
if (projectImportsCollector != null)
{
eventArgsWriter.EmbedFile += EventArgsWriter_EmbedFile;
}
if (replayEventSource != null)
{
if (CollectProjectImports == ProjectImportsCollectionMode.Embed)
{
replayEventSource.EmbeddedContentRead += args =>
eventArgsWriter.WriteBlob(args.ContentKind, args.ContentStream);
}
else if (CollectProjectImports == ProjectImportsCollectionMode.ZipFile)
{
replayEventSource.EmbeddedContentRead += args =>
ProjectImportsCollector.FlushBlobToFile(FilePath, args.ContentStream);
}
// If raw events are provided - let's try to use the advantage.
// But other subscribers can later on subscribe to structured events -
// for this reason we do only subscribe delayed.
replayEventSource.DeferredInitialize(
// For raw events we cannot write the initial info - as we cannot write
// at the same time as raw events are being written - this would break the deduplicated strings store.
// But we need to write the version info - but since we read/write raw - let's not change the version info.
() =>
{
binaryWriter.Write(replayEventSource.FileFormatVersion);
binaryWriter.Write(replayEventSource.MinimumReaderVersion);
replayEventSource.RawLogRecordReceived += RawEvents_LogDataSliceReceived;
// Replay separated strings here as well (and do not deduplicate! It would skew string indexes)
replayEventSource.StringReadDone += strArg => eventArgsWriter.WriteStringRecord(strArg.StringToBeUsed);
},
SubscribeToStructuredEvents);
}
else
{
SubscribeToStructuredEvents();
}
KnownTelemetry.LoggingConfigurationTelemetry.BinaryLogger = true;
void SubscribeToStructuredEvents()
{
// Write the version info - the latest version is written only for structured events replaying
// as raw events do not change structure - hence the version is the same as the one they were written with.
binaryWriter.Write(FileFormatVersion);
binaryWriter.Write(MinimumReaderVersion);
if (!omitInitialInfo)
{
LogInitialInfo();
}
eventSource.AnyEventRaised += EventSource_AnyEventRaised;
}
}
private void EventArgsWriter_EmbedFile(string filePath)
{
if (projectImportsCollector != null)
{
projectImportsCollector.AddFile(filePath);
}
}
private void LogInitialInfo()
{
LogMessage("BinLogFilePath=" + FilePath);
LogMessage("CurrentUICulture=" + System.Globalization.CultureInfo.CurrentUICulture.Name);
}
private void LogMessage(string text)
{
var args = new BuildMessageEventArgs(text, helpKeyword: null, senderName: "BinaryLogger", MessageImportance.Normal);
args.BuildEventContext = BuildEventContext.Invalid;
Write(args);
}
/// <summary>
/// Closes the underlying file stream.
/// </summary>
public void Shutdown()
{
Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", _initialTargetOutputLogging ? "true" : null);
Environment.SetEnvironmentVariable("MSBUILDLOGIMPORTS", _initialLogImports ? "1" : null);
Environment.SetEnvironmentVariable("MSBUILDBINARYLOGGERENABLED", _initialIsBinaryLoggerEnabled);
Traits.Instance.EscapeHatches.LogProjectImports = _initialLogImports;
Traits.Instance.EnableTargetOutputLogging = _initialTargetOutputLogging;
if (projectImportsCollector != null)
{
// Write the build check editorconfig file paths to the log
foreach (var filePath in EditorConfigParser.EditorConfigFilePaths)
{
projectImportsCollector.AddFile(filePath);
}
EditorConfigParser.ClearEditorConfigFilePaths();
projectImportsCollector.Close();
if (CollectProjectImports == ProjectImportsCollectionMode.Embed)
{
projectImportsCollector.ProcessResult(
streamToEmbed => eventArgsWriter.WriteBlob(BinaryLogRecordKind.ProjectImportArchive, streamToEmbed),
LogMessage);
projectImportsCollector.DeleteArchive();
}
projectImportsCollector.FileIOExceptionEvent -= EventSource_AnyEventRaised;
projectImportsCollector = null;
}
// Log additional file paths before closing stream (so they're recorded in the binlog)
if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0 && stream != null)
{
foreach (var additionalPath in AdditionalFilePaths)
{
LogMessage("BinLogCopyDestination=" + additionalPath);
}
}
if (stream != null)
{
// It's hard to determine whether we're at the end of decoding GZipStream
// so add an explicit 0 at the end to signify end of file
stream.WriteByte((byte)BinaryLogRecordKind.EndOfFile);
stream.Flush();
stream.Dispose();
stream = null;
}
// Copy the binlog file to additional destinations if specified
if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0)
{
foreach (var additionalPath in AdditionalFilePaths)
{
try
{
string directory = Path.GetDirectoryName(additionalPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.Copy(FilePath, additionalPath, overwrite: true);
}
catch (Exception ex)
{
// Log the error but don't fail the build
// Note: We can't use LogMessage here since the stream is already closed
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(
out _,
out _,
"ErrorCopyingBinaryLog",
FilePath,
additionalPath,
ex.Message);
Console.Error.WriteLine(message);
}
}
}
}
private void RawEvents_LogDataSliceReceived(BinaryLogRecordKind recordKind, Stream stream)
{
eventArgsWriter.WriteBlob(recordKind, stream);
}
private void EventSource_AnyEventRaised(object sender, BuildEventArgs e)
{
Write(e);
}
private void Write(BuildEventArgs e)
{
if (stream != null)
{
if (projectImportsCollector != null)
{
CollectImports(e);
}
if (DoNotWriteToBinlog(e))
{
return;
}
// TODO: think about queuing to avoid contention
lock (eventArgsWriter)
{
eventArgsWriter.Write(e);
}
}
}
private static bool DoNotWriteToBinlog(BuildEventArgs e)
{
return e is GeneratedFileUsedEventArgs;
}
private void CollectImports(BuildEventArgs e)
{
if (e is ProjectImportedEventArgs importArgs && importArgs.ImportedProjectFile != null)
{
projectImportsCollector.AddFile(importArgs.ImportedProjectFile);
}
else if (e is ProjectStartedEventArgs projectArgs)
{
projectImportsCollector.AddFile(projectArgs.ProjectFile);
}
else if (e is MetaprojectGeneratedEventArgs { metaprojectXml: { } } metaprojectArgs)
{
projectImportsCollector.AddFileFromMemory(metaprojectArgs.ProjectFile, metaprojectArgs.metaprojectXml);
}
else if (e is ResponseFileUsedEventArgs responseFileArgs && responseFileArgs.ResponseFilePath != null)
{
projectImportsCollector.AddFile(responseFileArgs.ResponseFilePath);
}
else if (e is GeneratedFileUsedEventArgs generatedFileUsedEventArgs && generatedFileUsedEventArgs.FilePath != null)
{
string fullPath = Path.GetFullPath(generatedFileUsedEventArgs.FilePath);
projectImportsCollector.AddFileFromMemory(fullPath, generatedFileUsedEventArgs.Content);
}
}
/// <summary>
/// Processes the parameters given to the logger from MSBuild.
/// </summary>
/// <exception cref="LoggerException">
/// </exception>
private void ProcessParameters(out bool omitInitialInfo)
{
var parsedParams = ParseParameters(Parameters);
omitInitialInfo = parsedParams.OmitInitialInfo;
// Only set CollectProjectImports if it was explicitly specified in parameters
if (parsedParams.HasProjectImportsParameter)
{
CollectProjectImports = parsedParams.ProjectImportsCollectionMode;
}
// Handle the file path - expand wildcards if needed
if (parsedParams.LogFilePath == null)
{
// Either no path was specified, or it contained wildcards
// Check if any parameter was a wildcard path
var parameters = Parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
foreach (var parameter in parameters)
{
if (TryInterpretPathParameter(parameter, out string filePath))
{
FilePath = filePath;
break;
}
}
}
else
{
FilePath = parsedParams.LogFilePath;
}
if (FilePath == null)
{
FilePath = "msbuild.binlog";
}
KnownTelemetry.LoggingConfigurationTelemetry.BinaryLoggerUsedDefaultName = FilePath == "msbuild.binlog";
try
{
FilePath = Path.GetFullPath(FilePath);
}
catch (Exception e)
{
string errorCode;
string helpKeyword;
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidFileLoggerFile", FilePath, e.Message);
throw new LoggerException(message, e, errorCode, helpKeyword);
}
}
private bool TryInterpretPathParameter(string parameter, out string filePath)
{
return TryInterpretPathParameterCore(parameter, GetUniqueStamp, out filePath);
}
private string GetUniqueStamp()
=> (PathParameterExpander ?? ExpandPathParameter)(string.Empty);
private static string ExpandPathParameter(string parameters)
=> $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}";
/// <summary>
/// Extracts the file path from binary logger parameters string.
/// This is a helper method for processing multiple binlog parameters.
/// </summary>
/// <param name="parameters">The parameters string (e.g., "output.binlog" or "output.binlog;ProjectImports=None")</param>
/// <returns>The resolved file path, or "msbuild.binlog" if no path is specified</returns>
public static string ExtractFilePathFromParameters(string parameters)
{
const string DefaultBinlogFileName = "msbuild" + BinlogFileExtension;
if (string.IsNullOrEmpty(parameters))
{
return Path.GetFullPath(DefaultBinlogFileName);
}
var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
string filePath = null;
foreach (var parameter in paramParts)
{
if (TryInterpretPathParameterStatic(parameter, out string extractedPath))
{
filePath = extractedPath;
break;
}
}
if (filePath == null)
{
filePath = DefaultBinlogFileName;
}
try
{
return Path.GetFullPath(filePath);
}
catch
{
// If path resolution fails, return the original path
return filePath;
}
}
/// <summary>
/// Attempts to interpret a parameter string as a file path.
/// </summary>
/// <param name="parameter">The parameter to interpret (e.g., "LogFile=output.binlog" or "output.binlog")</param>
/// <param name="filePath">The extracted file path if the parameter is a path, otherwise the original parameter</param>
/// <returns>True if the parameter is a valid file path (ends with .binlog or contains wildcards), false otherwise</returns>
private static bool TryInterpretPathParameterStatic(string parameter, out string filePath)
{
return TryInterpretPathParameterCore(parameter, () => ExpandPathParameter(string.Empty), out filePath);
}
/// <summary>
/// Core logic for interpreting a parameter string as a file path.
/// </summary>
/// <param name="parameter">The parameter to interpret</param>
/// <param name="wildcardExpander">Function to expand wildcard placeholders</param>
/// <param name="filePath">The extracted file path</param>
/// <returns>True if the parameter is a valid file path</returns>
private static bool TryInterpretPathParameterCore(string parameter, Func<string> wildcardExpander, out string filePath)
{
bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase);
if (hasPathPrefix)
{
parameter = parameter.Substring(LogFileParameterPrefix.Length);
}
parameter = parameter.Trim('"');
bool isWildcard = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12) && parameter.Contains("{}");
bool hasProperExtension = parameter.EndsWith(BinlogFileExtension, StringComparison.OrdinalIgnoreCase);
filePath = parameter;
if (!isWildcard)
{
return hasProperExtension;
}
filePath = parameter.Replace("{}", wildcardExpander(), StringComparison.Ordinal);
if (!hasProperExtension)
{
filePath += BinlogFileExtension;
}
return true;
}
/// <summary>
/// Extracts the non-file-path parameters from binary logger parameters string.
/// This is used to compare configurations between multiple binlog parameters.
/// </summary>
/// <param name="parameters">The parameters string (e.g., "output.binlog;ProjectImports=None")</param>
/// <returns>A normalized string of non-path parameters, or empty string if only path parameters</returns>
public static string ExtractNonPathParameters(string parameters)
{
if (string.IsNullOrEmpty(parameters))
{
return string.Empty;
}
var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
var nonPathParams = new List<string>();
foreach (var parameter in paramParts)
{
// Skip file path parameters
if (TryInterpretPathParameterStatic(parameter, out _))
{
continue;
}
// This is a configuration parameter (like ProjectImports=None, OmitInitialInfo, etc.)
nonPathParams.Add(parameter);
}
// Sort for consistent comparison
nonPathParams.Sort(StringComparer.OrdinalIgnoreCase);
return string.Join(";", nonPathParams);
}
/// <summary>
/// Result of processing multiple binary logger parameter sets.
/// </summary>
public readonly struct ProcessedBinaryLoggerParameters
{
/// <summary>
/// List of distinct parameter sets that need separate logger instances.
/// </summary>
public IReadOnlyList<string> DistinctParameterSets { get; }
/// <summary>
/// If true, all parameter sets have identical configurations (only file paths differ),
/// so a single logger can be used with file copying for additional paths.
/// </summary>
public bool AllConfigurationsIdentical { get; }
/// <summary>
/// Additional file paths to copy the binlog to (only valid when AllConfigurationsIdentical is true).
/// </summary>
public IReadOnlyList<string> AdditionalFilePaths { get; }
/// <summary>
/// List of duplicate file paths that were filtered out.
/// </summary>
public IReadOnlyList<string> DuplicateFilePaths { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ProcessedBinaryLoggerParameters"/> struct.
/// </summary>
/// <param name="distinctParameterSets">List of distinct parameter sets that need separate logger instances.</param>
/// <param name="allConfigurationsIdentical">Whether all parameter sets have identical configurations.</param>
/// <param name="additionalFilePaths">Additional file paths to copy the binlog to.</param>
/// <param name="duplicateFilePaths">List of duplicate file paths that were filtered out.</param>
public ProcessedBinaryLoggerParameters(
IReadOnlyList<string> distinctParameterSets,
bool allConfigurationsIdentical,
IReadOnlyList<string> additionalFilePaths,
IReadOnlyList<string> duplicateFilePaths)
{
DistinctParameterSets = distinctParameterSets;
AllConfigurationsIdentical = allConfigurationsIdentical;
AdditionalFilePaths = additionalFilePaths;
DuplicateFilePaths = duplicateFilePaths;
}
}
/// <summary>
/// Processes multiple binary logger parameter sets and returns distinct paths and configuration info.
/// </summary>
/// <param name="binaryLoggerParameters">Array of parameter strings from command line</param>
/// <returns>Processed result with distinct parameter sets and configuration info</returns>
public static ProcessedBinaryLoggerParameters ProcessParameters(string[] binaryLoggerParameters)
{
var distinctParameterSets = new List<string>();
var additionalFilePaths = new List<string>();
var duplicateFilePaths = new List<string>();
bool allConfigurationsIdentical = true;
if (binaryLoggerParameters == null || binaryLoggerParameters.Length == 0)
{
return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths);
}
if (binaryLoggerParameters.Length == 1)
{
distinctParameterSets.Add(binaryLoggerParameters[0]);
return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths);
}
string primaryArguments = binaryLoggerParameters[0];
string primaryNonPathParams = ExtractNonPathParameters(primaryArguments);
var distinctFilePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var extractedFilePaths = new List<string>();
// Check if all parameter sets have the same non-path configuration
for (int i = 0; i < binaryLoggerParameters.Length; i++)
{
string currentParams = binaryLoggerParameters[i];
string currentNonPathParams = ExtractNonPathParameters(currentParams);
string currentFilePath = ExtractFilePathFromParameters(currentParams);
// Check if this is a duplicate file path
if (distinctFilePaths.Add(currentFilePath))
{
if (!string.Equals(primaryNonPathParams, currentNonPathParams, StringComparison.OrdinalIgnoreCase))
{
allConfigurationsIdentical = false;
}
distinctParameterSets.Add(currentParams);
extractedFilePaths.Add(currentFilePath);
}
else
{
// Track duplicate paths for logging
duplicateFilePaths.Add(currentFilePath);
}
}
// If all configurations are identical, compute additional file paths for copying
// Use the pre-extracted paths to avoid redundant ExtractFilePathFromParameters calls
if (allConfigurationsIdentical && distinctParameterSets.Count > 1)
{
for (int i = 1; i < extractedFilePaths.Count; i++)
{
additionalFilePaths.Add(extractedFilePaths[i]);
}
}
return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths);
}
}
}
|