|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
namespace Microsoft.Build.Logging
{
/// <summary>
/// Interface for replaying a binary log file (*.binlog)
/// </summary>
internal interface IBinaryLogReplaySource :
IEventSource,
IBuildEventArgsReaderNotifications
{
/// <summary>
/// Event raised when non-textual log record is read.
/// This means all event args and key-value pairs.
/// Strings and Embedded files are not included.
/// </summary>
event Action<BinaryLogRecordKind, Stream>? RawLogRecordReceived;
/// <summary>
/// Enables initialization (e.g. subscription to events) - that is deferred until Replay is triggered.
/// At this point all other possible subscribers should be already subscribed -
/// so it can be determined if raw events or structured events should be replayed.
/// </summary>
/// <param name="onRawReadingPossible"></param>
/// <param name="onStructuredReadingOnly"></param>
void DeferredInitialize(
Action onRawReadingPossible,
Action onStructuredReadingOnly);
/// <summary>
/// File format version of the binary log file.
/// </summary>
int FileFormatVersion { get; }
/// <summary>
/// The minimum reader version for the binary log file.
/// </summary>
int MinimumReaderVersion { get; }
/// <summary>
/// Raised when the log reader encounters a project import archive (embedded content) in the stream.
/// The subscriber must read the exactly given length of binary data from the stream - otherwise exception is raised.
/// If no subscriber is attached, the data is skipped.
/// </summary>
event Action<EmbeddedContentEventArgs> EmbeddedContentRead;
}
/// <summary>
/// Provides a method to read a binary log file (*.binlog) and replay all stored BuildEventArgs
/// by implementing IEventSource and raising corresponding events.
/// </summary>
/// <remarks>The class is public so that we can call it from MSBuild.exe when replaying a log file.</remarks>
public sealed class BinaryLogReplayEventSource :
EventArgsDispatcher,
IBinaryLogReplaySource
{
private int? _fileFormatVersion;
private int? _minimumReaderVersion;
public int FileFormatVersion => _fileFormatVersion ?? throw new InvalidOperationException(ResourceUtilities.GetResourceString("Binlog_Source_VersionUninitialized"));
public int MinimumReaderVersion => _minimumReaderVersion ?? throw new InvalidOperationException(ResourceUtilities.GetResourceString("Binlog_Source_VersionUninitialized"));
/// Touches the <see cref="ItemGroupLoggingHelper"/> static constructor
/// to ensure it initializes <see cref="TaskParameterEventArgs.MessageGetter"/>
/// and <see cref="TaskParameterEventArgs.DictionaryFactory"/>
static BinaryLogReplayEventSource()
{
_ = ItemGroupLoggingHelper.ItemGroupIncludeLogMessagePrefix;
}
/// <summary>
/// Unknown build events or unknown parts of known build events will be ignored if this is set to true.
/// </summary>
public bool AllowForwardCompatibility { private get; init; }
/// <inheritdoc cref="IBuildEventArgsReaderNotifications.RecoverableReadError"/>
public event Action<BinaryLogReaderErrorEventArgs>? RecoverableReadError;
/// <summary>
/// Read the provided binary log file and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="sourceFilePath">The full file path of the binary log file</param>
public void Replay(string sourceFilePath)
{
Replay(sourceFilePath, CancellationToken.None);
}
/// <summary>
/// Read the provided binary log file opened as a stream and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="sourceFileStream">Stream over the binlog content.</param>
/// <param name="cancellationToken"></param>
public void Replay(Stream sourceFileStream, CancellationToken cancellationToken)
{
using var binaryReader = OpenReader(sourceFileStream);
Replay(binaryReader, cancellationToken);
}
/// <summary>
/// Creates a <see cref="BinaryReader"/> for the provided binary log file.
/// Performs decompression and buffering in the optimal way.
/// Caller is responsible for disposing the returned reader.
/// </summary>
/// <param name="sourceFilePath"></param>
/// <returns>BinaryReader of the given binlog file.</returns>
public static BinaryReader OpenReader(string sourceFilePath)
{
Stream? stream = null;
try
{
stream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return OpenReader(stream);
}
catch (Exception)
{
stream?.Dispose();
throw;
}
}
/// <summary>
/// Creates a <see cref="BinaryReader"/> for the provided binary log file.
/// Performs decompression and buffering in the optimal way.
/// Caller is responsible for disposing the returned reader.
/// </summary>
/// <param name="sourceFileStream">Stream over the binlog file</param>
/// <returns>BinaryReader of the given binlog file.</returns>
public static BinaryReader OpenReader(Stream sourceFileStream)
{
var gzipStream = new GZipStream(sourceFileStream, CompressionMode.Decompress, leaveOpen: false);
// 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
var bufferedStream = new BufferedStream(gzipStream, 32768);
return new BinaryReader(bufferedStream);
}
/// <summary>
/// Creates a <see cref="BuildEventArgsReader"/> for the provided binary reader over binary log file.
/// Caller is responsible for disposing the returned reader.
/// </summary>
/// <param name="binaryReader"></param>
/// <param name="closeInput">Indicates whether the passed BinaryReader should be closed on disposing.</param>
/// <param name="allowForwardCompatibility">Unknown build events or unknown parts of known build events will be ignored if this is set to true.</param>
/// <returns>BuildEventArgsReader over the given binlog file binary reader.</returns>
public static BuildEventArgsReader OpenBuildEventsReader(
BinaryReader binaryReader,
bool closeInput,
bool allowForwardCompatibility = false)
{
int fileFormatVersion = binaryReader.ReadInt32();
// Is this the new log format that contains the minimum reader version?
int minimumReaderVersion = fileFormatVersion >= BinaryLogger.ForwardCompatibilityMinimalVersion
? binaryReader.ReadInt32()
: fileFormatVersion;
// the log file is written using a newer version of file format
// that we don't know how to read
if (fileFormatVersion > BinaryLogger.FileFormatVersion &&
(!allowForwardCompatibility || minimumReaderVersion > BinaryLogger.FileFormatVersion))
{
var text = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("UnsupportedLogFileFormat", fileFormatVersion, minimumReaderVersion, BinaryLogger.FileFormatVersion);
throw new NotSupportedException(text);
}
return new BuildEventArgsReader(binaryReader, fileFormatVersion)
{
CloseInput = closeInput,
MinimumReaderVersion = minimumReaderVersion
};
}
/// <summary>
/// Creates a <see cref="BinaryReader"/> for the provided binary log file.
/// Performs decompression and buffering in the optimal way.
/// Caller is responsible for disposing the returned reader.
/// </summary>
/// <param name="sourceFilePath"></param>
/// <returns>BinaryReader of the given binlog file.</returns>
public static BuildEventArgsReader OpenBuildEventsReader(string sourceFilePath)
=> OpenBuildEventsReader(OpenReader(sourceFilePath), true);
/// <summary>
/// Read the provided binary log file and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="sourceFilePath">The full file path of the binary log file</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> indicating the replay should stop as soon as possible.</param>
public void Replay(string sourceFilePath, CancellationToken cancellationToken)
{
using var eventsReader = OpenBuildEventsReader(sourceFilePath);
Replay(eventsReader, cancellationToken);
}
/// <summary>
/// Read the provided binary log file and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="binaryReader">The binary log content binary reader - caller is responsible for disposing.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> indicating the replay should stop as soon as possible.</param>
public void Replay(BinaryReader binaryReader, CancellationToken cancellationToken)
=> Replay(binaryReader, false, cancellationToken);
/// <summary>
/// Read the provided binary log file and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="binaryReader">The binary log content binary reader - caller is responsible for disposing, unless <paramref name="closeInput"/> is set to true.</param>
/// <param name="closeInput">Indicates whether the passed BinaryReader should be closed on disposing.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> indicating the replay should stop as soon as possible.</param>
public void Replay(BinaryReader binaryReader, bool closeInput, CancellationToken cancellationToken)
{
using var reader = OpenBuildEventsReader(binaryReader, closeInput, AllowForwardCompatibility);
Replay(reader, cancellationToken);
}
/// <summary>
/// Read the provided binary log file and raise corresponding events for each BuildEventArgs
/// </summary>
/// <param name="reader">The build events reader - caller is responsible for disposing.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> indicating the replay should stop as soon as possible.</param>
public void Replay(BuildEventArgsReader reader, CancellationToken cancellationToken)
{
_fileFormatVersion = reader.FileFormatVersion;
_minimumReaderVersion = reader.MinimumReaderVersion;
bool supportsForwardCompatibility = reader.FileFormatVersion >= BinaryLogger.ForwardCompatibilityMinimalVersion;
// Allow any possible deferred subscriptions to be registered
if (HasStructuredEventsSubscribers || !supportsForwardCompatibility)
{
_onStructuredReadingOnly?.Invoke();
}
else
{
_onRawReadingPossible?.Invoke();
}
reader.EmbeddedContentRead += _embeddedContentRead;
reader.ArchiveFileEncountered += _archiveFileEncountered;
reader.StringReadDone += _stringReadDone;
if (HasStructuredEventsSubscribers || !supportsForwardCompatibility)
{
if (this._rawLogRecordReceived != null)
{
throw new NotSupportedException(
ResourceUtilities.GetResourceString("Binlog_Source_MultiSubscribeError"));
}
// Forward compatible reading makes sense only for structured events reading.
reader.SkipUnknownEvents = supportsForwardCompatibility && AllowForwardCompatibility;
reader.SkipUnknownEventParts = supportsForwardCompatibility && AllowForwardCompatibility;
reader.RecoverableReadError += RecoverableReadError;
while (!cancellationToken.IsCancellationRequested && reader.Read() is { } instance)
{
Dispatch(instance);
}
}
else
{
if (this._rawLogRecordReceived == null &&
this._embeddedContentRead == null &&
this._stringReadDone == null &&
this._archiveFileEncountered == null)
{
throw new NotSupportedException(
ResourceUtilities.GetResourceString("Binlog_Source_MissingSubscribeError"));
}
while (!cancellationToken.IsCancellationRequested && reader.ReadRaw() is { } instance &&
instance.RecordKind != BinaryLogRecordKind.EndOfFile)
{
_rawLogRecordReceived?.Invoke(instance.RecordKind, instance.Stream);
}
}
// Unsubscribe from events for a case if the reader is reused (upon cancellation).
reader.EmbeddedContentRead -= _embeddedContentRead;
reader.ArchiveFileEncountered -= _archiveFileEncountered;
reader.StringReadDone -= _stringReadDone;
reader.RecoverableReadError -= RecoverableReadError;
}
// Following members are explicit implementations of the IBinaryLogReplaySource interface
// to avoid exposing them publicly.
// We want an interface so that BinaryLogger can fine tune its initialization logic
// in case the given event source is the replay source. On the other hand we don't want
// to expose these members publicly because they are not intended to be used by the consumers.
private Action? _onRawReadingPossible;
private Action? _onStructuredReadingOnly;
/// <inheritdoc cref="IBinaryLogReplaySource.DeferredInitialize"/>
void IBinaryLogReplaySource.DeferredInitialize(
Action onRawReadingPossible,
Action onStructuredReadingOnly)
{
this._onRawReadingPossible += onRawReadingPossible;
this._onStructuredReadingOnly += onStructuredReadingOnly;
}
private Action<EmbeddedContentEventArgs>? _embeddedContentRead;
/// <inheritdoc cref="IBinaryLogReplaySource.EmbeddedContentRead"/>
event Action<EmbeddedContentEventArgs>? IBinaryLogReplaySource.EmbeddedContentRead
{
// Explicitly implemented event has to declare explicit add/remove accessors
// https://stackoverflow.com/a/2268472/2308106
add => _embeddedContentRead += value;
remove => _embeddedContentRead -= value;
}
private Action<StringReadEventArgs>? _stringReadDone;
/// <inheritdoc cref="IBuildEventArgsReaderNotifications.StringReadDone"/>
event Action<StringReadEventArgs>? IBuildEventArgsReaderNotifications.StringReadDone
{
add => _stringReadDone += value;
remove => _stringReadDone -= value;
}
private Action<ArchiveFileEventArgs>? _archiveFileEncountered;
/// <inheritdoc cref="IBuildEventArgsReaderNotifications.ArchiveFileEncountered"/>
event Action<ArchiveFileEventArgs>? IBuildEventArgsReaderNotifications.ArchiveFileEncountered
{
add => _archiveFileEncountered += value;
remove => _archiveFileEncountered -= value;
}
private Action<BinaryLogRecordKind, Stream>? _rawLogRecordReceived;
/// <inheritdoc cref="IBinaryLogReplaySource.RawLogRecordReceived"/>
event Action<BinaryLogRecordKind, Stream>? IBinaryLogReplaySource.RawLogRecordReceived
{
add => _rawLogRecordReceived += value;
remove => _rawLogRecordReceived -= value;
}
}
}
|