File: EditAndContinue\TraceLog.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
/// <summary>
/// Fixed size rolling tracing log. 
/// </summary>
/// <remarks>
/// Recent entries are captured in a memory dump.
/// If DEBUG is defined, all entries written to <see cref="DebugWrite(string)"/> or
/// <see cref="DebugWrite(string, Arg[])"/> are print to <see cref="Debug"/> output.
/// </remarks>
internal sealed class TraceLog(int logSize, string id, string fileName)
{
    internal readonly struct Arg
    {
        // To display enums in Expression Evaluator we need to remember the type of the enum.
        // The debugger currently does not support evaluating expressions that involve Type instances nor lambdas,
        // so we need to manually special case the types of enums we care about displaying.
 
        private enum EnumType
        {
            ProjectAnalysisSummary,
            RudeEditKind,
            ModuleUpdateStatus,
            EditAndContinueCapabilities,
        }
 
        private static readonly StrongBox<EnumType> s_ProjectAnalysisSummary = new(EnumType.ProjectAnalysisSummary);
        private static readonly StrongBox<EnumType> s_RudeEditKind = new(EnumType.RudeEditKind);
        private static readonly StrongBox<EnumType> s_ModuleUpdateStatus = new(EnumType.ModuleUpdateStatus);
        private static readonly StrongBox<EnumType> s_EditAndContinueCapabilities = new(EnumType.EditAndContinueCapabilities);
 
        public readonly object? Object;
        public readonly int Int32;
        public readonly ImmutableArray<int> Tokens;
 
        public Arg(object? value)
        {
            Int32 = -1;
            Object = value ?? "<null>";
            Tokens = default;
        }
 
        public Arg(ImmutableArray<int> tokens)
        {
            Int32 = -1;
            Object = null;
            Tokens = tokens;
        }
 
        private Arg(int value, StrongBox<EnumType> enumKind)
        {
            Int32 = value;
            Object = enumKind;
            Tokens = default;
        }
 
        public object? GetDebuggerDisplay()
            => (!Tokens.IsDefault) ? string.Join(",", Tokens.Select(token => token.ToString("X8"))) :
               (Object is ImmutableArray<string> array) ? string.Join(",", array) :
               (Object is null) ? Int32 :
               (Object is StrongBox<EnumType> { Value: var enumType }) ? enumType switch
               {
                   EnumType.ProjectAnalysisSummary => (ProjectAnalysisSummary)Int32,
                   EnumType.RudeEditKind => (RudeEditKind)Int32,
                   EnumType.ModuleUpdateStatus => (ModuleUpdateStatus)Int32,
                   EnumType.EditAndContinueCapabilities => (EditAndContinueCapabilities)Int32,
                   _ => throw ExceptionUtilities.UnexpectedValue(enumType)
               } :
               Object;
 
        public static implicit operator Arg(string? value) => new(value);
        public static implicit operator Arg(int value) => new(value);
        public static implicit operator Arg(bool value) => new(value ? "true" : "false");
        public static implicit operator Arg(ProjectId value) => new(value.DebugName);
        public static implicit operator Arg(DocumentId value) => new(value.DebugName);
        public static implicit operator Arg(Diagnostic value) => new(value.ToString());
        public static implicit operator Arg(ProjectAnalysisSummary value) => new((int)value, s_ProjectAnalysisSummary);
        public static implicit operator Arg(RudeEditKind value) => new((int)value, s_RudeEditKind);
        public static implicit operator Arg(ModuleUpdateStatus value) => new((int)value, s_ModuleUpdateStatus);
        public static implicit operator Arg(EditAndContinueCapabilities value) => new((int)value, s_EditAndContinueCapabilities);
        public static implicit operator Arg(ImmutableArray<int> tokens) => new(tokens);
        public static implicit operator Arg(ImmutableArray<string> items) => new(items);
    }
 
    [DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
    internal readonly struct Entry(string format, Arg[]? args)
    {
        public readonly string MessageFormat = format;
        public readonly Arg[]? Args = args;
 
        internal string GetDebuggerDisplay()
            => (MessageFormat == null) ? "" : string.Format(MessageFormat, Args?.Select(a => a.GetDebuggerDisplay()).ToArray() ?? []);
    }
 
    internal sealed class FileLogger(string logDirectory, TraceLog traceLog)
    {
        private readonly string _logDirectory = logDirectory;
        private readonly TraceLog _traceLog = traceLog;
 
        public void Append(Entry entry)
        {
            string? path = null;
 
            try
            {
                path = Path.Combine(_logDirectory, _traceLog._fileName);
                File.AppendAllLines(path, [entry.GetDebuggerDisplay()]);
            }
            catch (Exception e)
            {
                _traceLog.AppendFileLoggingErrorInMemory(path, e);
            }
        }
 
        private string CreateSessionDirectory(DebuggingSessionId sessionId, string relativePath)
        {
            Contract.ThrowIfNull(_logDirectory);
            var directory = Path.Combine(_logDirectory, sessionId.Ordinal.ToString(), relativePath);
            Directory.CreateDirectory(directory);
            return directory;
        }
 
        private string MakeSourceFileLogPath(Document document, string suffix, UpdateId updateId, int? generation)
        {
            Debug.Assert(document.FilePath != null);
            Debug.Assert(document.Project.FilePath != null);
 
            var projectDir = PathUtilities.GetDirectoryName(document.Project.FilePath)!;
            var documentDir = PathUtilities.GetDirectoryName(document.FilePath)!;
            var extension = PathUtilities.GetExtension(document.FilePath);
            var fileName = PathUtilities.GetFileName(document.FilePath, includeExtension: false);
 
            var relativeDir = PathUtilities.IsSameDirectoryOrChildOf(documentDir, projectDir) ? PathUtilities.GetRelativePath(projectDir, documentDir) : documentDir;
            relativeDir = relativeDir.Replace('\\', '_').Replace('/', '_');
 
            var directory = CreateSessionDirectory(updateId.SessionId, Path.Combine(document.Project.Name, relativeDir));
            return Path.Combine(directory, $"{fileName}.{updateId.Ordinal}.{generation?.ToString() ?? "-"}.{suffix}{extension}");
        }
 
        public void Write(DebuggingSessionId sessionId, ImmutableArray<byte> bytes, string directory, string fileName)
        {
            string? path = null;
            try
            {
                path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName);
                File.WriteAllBytes(path, [.. bytes]);
            }
            catch (Exception e)
            {
                _traceLog.AppendFileLoggingErrorInMemory(path, e);
            }
        }
 
        public async ValueTask WriteAsync(Func<Stream, CancellationToken, ValueTask> writer, DebuggingSessionId sessionId, string directory, string fileName, CancellationToken cancellationToken)
        {
            string? path = null;
            try
            {
                path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName);
                using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete);
                await writer(file, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e)
            {
                _traceLog.AppendFileLoggingErrorInMemory(path, e);
            }
        }
 
        public async ValueTask WriteDocumentAsync(Document document, string fileNameSuffix, UpdateId updateId, int? generation, CancellationToken cancellationToken)
        {
            Debug.Assert(document.FilePath != null);
 
            string? path = null;
            try
            {
                path = MakeSourceFileLogPath(document, fileNameSuffix, updateId, generation);
                var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write | FileShare.Delete);
                using var writer = new StreamWriter(file, text.Encoding ?? Encoding.UTF8);
                text.Write(writer, cancellationToken);
            }
            catch (Exception e)
            {
                _traceLog.AppendFileLoggingErrorInMemory(path, e);
            }
        }
 
        public async ValueTask WriteDocumentChangeAsync(Document? oldDocument, Document? newDocument, UpdateId updateId, int? generation, CancellationToken cancellationToken)
        {
            if (oldDocument?.FilePath != null)
            {
                await WriteDocumentAsync(oldDocument, fileNameSuffix: "old", updateId, generation, cancellationToken).ConfigureAwait(false);
            }
 
            if (newDocument?.FilePath != null)
            {
                await WriteDocumentAsync(newDocument, fileNameSuffix: "new", updateId, generation, cancellationToken).ConfigureAwait(false);
            }
        }
    }
 
    private readonly Entry[] _log = new Entry[logSize];
    private readonly string _id = id;
    private readonly string _fileName = fileName;
    private int _currentLine;
 
    public FileLogger? FileLog { get; private set; }
 
    public void SetLogDirectory(string? logDirectory)
    {
        FileLog = (logDirectory != null) ? new FileLogger(logDirectory, this) : null;
    }
 
    private void AppendInMemory(Entry entry)
    {
        var index = Interlocked.Increment(ref _currentLine);
        _log[(index - 1) % _log.Length] = entry;
    }
 
    private void AppendFileLoggingErrorInMemory(string? path, Exception e)
        => AppendInMemory(new Entry("Error writing log file '{0}': {1}", [new Arg(path), new Arg(e.Message)]));
 
    private void Append(Entry entry)
    {
        AppendInMemory(entry);
        FileLog?.Append(entry);
    }
 
    public void Write(string str)
        => Write(str, args: null);
 
    public void Write(string format, params Arg[]? args)
        => Append(new Entry(format, args));
 
    [Conditional("DEBUG")]
    public void DebugWrite(string str)
        => DebugWrite(str, args: null);
 
    [Conditional("DEBUG")]
    public void DebugWrite(string format, params Arg[]? args)
    {
        var entry = new Entry(format, args);
        Append(entry);
        Debug.WriteLine(entry.ToString(), _id);
    }
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor(TraceLog traceLog)
    {
        private readonly TraceLog _traceLog = traceLog;
 
        internal Entry[] Entries => _traceLog._log;
    }
}