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.Text;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
internal enum LogMessageSeverity
{
    Info,
    Warning,
    Error
}
 
/// <summary>
/// Implements EnC logging.
/// 
/// Writes log messages to:
/// - fixed size rolling tracing log captured in a memory dump,
/// - a file log, if a log directory is provided,
/// - log service, if avaiable.
/// </summary>
internal sealed class TraceLog(string name, IEditAndContinueLogReporter? logService = null, int logSize = 2048)
{
    internal sealed class FileLogger(string logDirectory, TraceLog traceLog)
    {
        private readonly string _logDirectory = logDirectory;
        private readonly TraceLog _traceLog = traceLog;
 
        public void Append(string entry)
        {
            string? path = null;
 
            try
            {
                path = Path.Combine(_logDirectory, _traceLog._name + ".log");
                File.AppendAllLines(path, [entry]);
            }
            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 string[] _log = new string[logSize];
    private readonly string _name = name;
    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(string entry)
    {
        var index = Interlocked.Increment(ref _currentLine);
        _log[(index - 1) % _log.Length] = entry;
    }
 
    private void AppendFileLoggingErrorInMemory(string? path, Exception e)
        => AppendInMemory($"Error writing log file '{path}': {e.Message}");
 
    public void Write(string message, LogMessageSeverity severity = LogMessageSeverity.Info)
    {
        AppendInMemory(message);
        FileLog?.Append(message);
        logService?.Report(message, severity);
    }
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor(TraceLog traceLog)
    {
        internal string[] Entries => traceLog._log;
    }
}