|
// 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.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Build.Framework;
using CommonWriterType = System.Action<string, string, System.Collections.Generic.IEnumerable<string>>;
#nullable disable
namespace Microsoft.Build.Shared.Debugging
{
/// <summary>
/// A class to help with printline debugging in difficult environments like CI, or when debugging msbuild through other
/// tools like VS or CLI.
/// See example usages in PrintLineDebugger_Tests
/// </summary>
internal sealed class PrintLineDebugger : IDisposable
{
private static readonly Lazy<PropertyInfo> CommonWriterProperty = new Lazy<PropertyInfo>(
() =>
{
var commonWriterType = typeof(ITask).Assembly.GetType("Microsoft.Build.Shared.Debugging.CommonWriter", true, false);
var propertyInfo = commonWriterType.GetProperty("Writer", BindingFlags.Public | BindingFlags.Static);
ErrorUtilities.VerifyThrowInternalNull(propertyInfo);
return propertyInfo;
});
public static Lazy<PrintLineDebugger> Default =
new Lazy<PrintLineDebugger>(() => Create(null, null, false));
public static Lazy<PrintLineDebugger> DefaultWithProcessInfo =
new Lazy<PrintLineDebugger>(() => Create(null, null, true));
private readonly string _id;
private readonly CommonWriterType _writerSetByThisInstance;
public PrintLineDebugger(string id, CommonWriterType writer)
{
_id = id ?? string.Empty;
if (writer != null)
{
SetWriter(writer);
// we wrap the original writer with a locking writer in SetWriter, so get the actual writer that was set
_writerSetByThisInstance = GetStaticWriter();
}
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
public static CommonWriterType GetStaticWriter()
{
return (CommonWriterType)CommonWriterProperty.Value.GetValue(null, null);
}
// this setter is not thread safe because the assumption is that a writer is set once for the duration of the process (or multiple times from different tests which do not run in parallel).
public static void SetWriter(CommonWriterType writer)
{
#if DEBUG
var currentWriter = GetStaticWriter();
if (currentWriter != null)
{
ErrorUtilities.ThrowInternalError("Cannot set a new writer over an old writer. Remove the old one first");
}
// wrap with a lock so multi threaded logging does not break messages apart
CommonWriterProperty.Value.SetValue(null, (CommonWriterType)LockWrappedWriter);
void LockWrappedWriter(string id, string callsite, IEnumerable<string> message)
{
lock (writer)
{
writer.Invoke(id, callsite, message);
}
}
#endif
}
public static void UnsetWriter()
{
var currentWriter = GetStaticWriter();
if (currentWriter == null)
{
ErrorUtilities.ThrowInternalError("Cannot unset an already null writer");
}
CommonWriterProperty.Value.SetValue(null, null);
}
public static PrintLineDebugger Create(
CommonWriterType writer = null,
string id = null,
bool prependProcessInfo = false)
{
return new PrintLineDebugger(
prependProcessInfo
? $"{DebugUtils.ProcessInfoString}_{id}"
: id,
writer);
}
public CommonWriterType GetWriter()
{
return _writerSetByThisInstance ?? GetStaticWriter();
}
public void Log(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
#if DEBUG
var writer = GetWriter();
writer?.Invoke(_id, CallsiteString(sourceFilePath, memberName, sourceLineNumber), [message]);
#endif
}
public void Log(
IEnumerable<string> args,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
#if DEBUG
var writer = GetWriter();
writer?.Invoke(_id, CallsiteString(sourceFilePath, memberName, sourceLineNumber), args);
#endif
}
private static string CallsiteString(string sourceFilePath, string memberName, int sourceLineNumber)
{
return $"@{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}({sourceLineNumber})";
}
private void ReleaseUnmanagedResources()
{
if (_writerSetByThisInstance != null)
{
var staticWriter = GetStaticWriter();
if (staticWriter != _writerSetByThisInstance)
{
ErrorUtilities.ThrowInternalError($"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer.");
}
UnsetWriter();
}
}
~PrintLineDebugger()
{
ReleaseUnmanagedResources();
}
}
}
|