File: Installer\Windows\TimestampedFileLogger.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Collections.Concurrent;
using System.Globalization;
using System.IO.Pipes;
using System.Runtime.Versioning;
 
namespace Microsoft.DotNet.Cli.Installer.Windows;
 
/// <summary>
/// Represents a log file whose entries are timestamped. Additional loggers from external processes
/// can send requests through named pipes to the logger. Each pipe is assigned a separate thread to
/// queue messages.
/// </summary>
[SupportedOSPlatform("windows")]
internal class TimestampedFileLogger : SetupLoggerBase, IDisposable, ISynchronizingLogger
{
    /// <summary>
    /// Thread safe queue use to store incoming log request messages.
    /// </summary>
    private readonly BlockingCollection<string> _messageQueue = [];
 
    private bool _disposed;
    private readonly StreamWriter _stream;
 
    /// <summary>
    /// The number of write operations that can execute before the underlying stream is flushed. The default
    /// value is 20.
    /// </summary>
    public readonly int FlushThreshold;
 
    /// <summary>
    /// The full path of the log file.
    /// </summary>
    public string LogPath
    {
        get;
    }
 
    private readonly Thread LogWriter;
 
    /// <summary>
    /// The locale-neutral timestamp prefix.
    /// </summary>
    protected static string TimeStamp => $"[{string.Create(CultureInfo.InvariantCulture, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}")}]";
 
    /// <summary>
    /// Creates a new <see cref="TimestampedFileLogger"/> instance.
    /// </summary>
    /// <param name="path">The path of the log file.</param>
    public TimestampedFileLogger(string path) : this(path, 20)
    {
 
    }
 
    /// <summary>
    /// Creates a new <see cref="TimestampedFileLogger"/> instance.
    /// </summary>
    /// <param name="path">The path of the log file.</param>
    /// <param name="flushThreshold">The number of writes to allow before flushing the underlying stream.</param>
    /// <param name="logPipeNames">Additional named pipes that can be used to send log requests from other processes.</param>
    public TimestampedFileLogger(string path, int flushThreshold, params string[] logPipeNames)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
        _stream = File.CreateText(path);
        LogPath = Path.GetFullPath(path);
        FlushThreshold = flushThreshold;
 
        // Spin up additional threads to listen for log requests coming in from external processes.
        foreach (string logPipeName in logPipeNames)
        {
            Thread logRequestThread = new(ProcessLogRequests) { IsBackground = true };
            logRequestThread.Start(logPipeName);
        }
 
        // The primary thread responsible for dequeuing messages and writing
        // messages to the log file.
        LogWriter = new Thread(WriteLog) { IsBackground = true };
        LogWriter.Start(FlushThreshold);
 
        LogMessage($"=== Logging started ===");
    }
 
    public void AddNamedPipe(string pipeName)
    {
        Thread logRequestThread = new(ProcessLogRequests) { IsBackground = true };
        logRequestThread.Start(pipeName);
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            _messageQueue.CompleteAdding();
            LogWriter?.Join();
            _stream.WriteLine($"{TimeStamp} {FormatMessage("=== Logging ended ===")}");
 
            if (disposing)
            {
                _stream?.Dispose();
                _messageQueue.Dispose();
            }
 
            _disposed = true;
        }
    }
 
    /// <summary>
    /// Thread used to process external log requests received from a single pipe.
    /// </summary>
    /// <param name="logPipeName"></param>
    private void ProcessLogRequests(object logPipeName)
    {
        NamedPipeClientStream logPipe = new(".", (string)logPipeName, PipeDirection.InOut);
        PipeStreamMessageDispatcherBase dispatcher = new(logPipe);
        dispatcher.Connect();
        LogMessage($"Log connected: {logPipeName}.");
 
        while (dispatcher.IsConnected)
        {
            try
            {
                // We'll block waiting for messages to arrive before sending them to the queue. We don't call LogMessage
                // directly since the external logger should have stamped the message with the process ID.
                string msg = Encoding.UTF8.GetString(dispatcher.ReadMessage());
 
                if (!string.IsNullOrWhiteSpace(msg))
                {
                    WriteMessage(msg);
                }
            }
            catch (Exception e)
            {
                LogMessage(e.Message);
            }
        }
 
        LogMessage($"Shutting down log processor. Thread ID: {Thread.CurrentThread.ManagedThreadId}, Dispatcher connected: {dispatcher.IsConnected}");
    }
 
    /// <summary>
    /// Primary thread for dequeuing messages, timestamping them, and writing them to the log file.
    /// </summary>
    private void WriteLog(object flushThreshold)
    {
        int writeCount = 0;
        int threshold = (int)flushThreshold;
 
        foreach (string message in _messageQueue.GetConsumingEnumerable())
        {
            _stream.WriteLine($"{TimeStamp} {message}");
            writeCount = (writeCount + 1) % threshold;
 
            if (writeCount == 0)
            {
                _stream.Flush();
            }
        }
    }
 
    /// <summary>
    /// Writes the specified message to the log file. The message will first be added to an internal queue to be timestamped
    /// before it's dequeued and written to the log.
    /// </summary>
    /// <param name="message">The message to log.</param>
    protected override void WriteMessage(string message)
    {
        if (!_messageQueue.IsAddingCompleted)
        {
            _messageQueue.Add(message);
        }
    }
}