File: Debugging\PrintLineDebugger.cs
Web Access
Project: Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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;
#if DEBUG
using System.IO;
#endif
using System.Runtime.CompilerServices;
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
{
    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 static CommonWriterType s_writer;

#if DEBUG
    private readonly string _id;
#endif

    private readonly CommonWriterType _writerSetByThisInstance;

    public PrintLineDebugger(string id, CommonWriterType writer)
    {
#if DEBUG
        _id = id ?? string.Empty;
#endif

        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() => s_writer;

    // 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();

        Assumed.Null(currentWriter, "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
        s_writer = 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();

        Assumed.NotNull(currentWriter, "Cannot unset an already null writer");

        s_writer = 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
    }

#if DEBUG
    private static string CallsiteString(string sourceFilePath, string memberName, int sourceLineNumber)
    {
        return $"@{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}({sourceLineNumber})";
    }
#endif

    private void ReleaseUnmanagedResources()
    {
        if (_writerSetByThisInstance != null)
        {
            var staticWriter = GetStaticWriter();

            Assumed.Equal(staticWriter, _writerSetByThisInstance, $"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer.");

            UnsetWriter();
        }
    }

    ~PrintLineDebugger()
    {
        ReleaseUnmanagedResources();
    }
}