File: Logging\OutputWindowLoggerProvider.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudio.LanguageServices.Razor\Microsoft.VisualStudio.LanguageServices.Razor.csproj (Microsoft.VisualStudio.LanguageServices.Razor)
// 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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Workspaces.Settings;
using Microsoft.VisualStudio.Razor.Extensions;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
 
namespace Microsoft.VisualStudio.Razor.Logging;
 
[ExportLoggerProvider]
[method: ImportingConstructor]
internal class OutputWindowLoggerProvider(
    // Anything this class imports would have a circular dependency if they tried to log anything,
    // or used anything that does logging, so make sure everything of ours is imported lazily
    Lazy<IClientSettingsManager> clientSettingsManager,
    JoinableTaskContext joinableTaskContext)
    : ILoggerProvider, IDisposable
{
    private readonly Lazy<IClientSettingsManager> _clientSettingsManager = clientSettingsManager;
    private readonly OutputPane _outputPane = new OutputPane(joinableTaskContext);
 
    public ILogger CreateLogger(string categoryName)
    {
        return new OutputPaneLogger(categoryName, _outputPane, _clientSettingsManager.Value);
    }
 
    public void Dispose()
    {
        _outputPane.Dispose();
    }
 
    private class OutputPaneLogger : ILogger
    {
        private readonly string _categoryName;
        private readonly OutputPane _outputPane;
        private readonly IClientSettingsManager _clientSettingsManager;
 
        public OutputPaneLogger(string categoryName, OutputPane outputPane, IClientSettingsManager clientSettingsManager)
        {
            _categoryName = categoryName;
            _outputPane = outputPane;
            _clientSettingsManager = clientSettingsManager;
        }
 
        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _clientSettingsManager.GetClientSettings().AdvancedSettings.LogLevel;
        }
 
        public void Log(LogLevel logLevel, string message, Exception? exception)
        {
            if (IsEnabled(logLevel))
            {
                var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception);
                _outputPane.WriteLine(formattedMessage);
            }
        }
    }
 
    private class OutputPane : IDisposable
    {
        private static readonly Guid s_outputPaneGuid = new("BBAFF416-4AF5-41F2-9F93-91F283E43C3B");
 
        private readonly JoinableTaskContext _threadingContext;
        private readonly IServiceProvider _serviceProvider;
        private readonly AsyncQueue<string> _outputQueue;
        private readonly CancellationTokenSource _disposalTokenSource;
        private IVsOutputWindowPane? _doNotAccessDirectlyOutputPane;
 
        public OutputPane(JoinableTaskContext threadingContext)
        {
            _threadingContext = threadingContext;
            _serviceProvider = ServiceProvider.GlobalProvider;
 
            _outputQueue = new AsyncQueue<string>();
            _disposalTokenSource = new CancellationTokenSource();
 
            _ = StartListeningAsync();
        }
 
        private async Task StartListeningAsync()
        {
            // Ensure that we're never on the UI thread before we start listening, in case the async queue doesn't yield
            // I suspect this is overkill :D
            await TaskScheduler.Default.SwitchTo(alwaysYield: true);
 
            while (!_disposalTokenSource.IsCancellationRequested)
            {
                await DequeueAsync(_disposalTokenSource.Token).ConfigureAwait(false);
            }
        }
 
        public void WriteLine(string value)
        {
            _outputQueue.TryEnqueue(value);
        }
 
        private async Task DequeueAsync(CancellationToken cancellationToken)
        {
            var value = await _outputQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
            if (value is null || cancellationToken.IsCancellationRequested)
            {
                return;
            }
 
            await _threadingContext.Factory.SwitchToMainThreadAsync(cancellationToken);
 
            var pane = GetPane();
            if (pane is null)
            {
                return;
            }
 
            // https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivsoutputwindowpane.outputstringthreadsafe?view=visualstudiosdk-2022#remarks
            if (pane is IVsOutputWindowPaneNoPump noPumpPane)
            {
                noPumpPane.OutputStringNoPump(value + Environment.NewLine);
            }
            else
            {
                pane.OutputStringThreadSafe(value + Environment.NewLine);
            }
        }
 
        private IVsOutputWindowPane GetPane()
        {
            _threadingContext.AssertUIThread();
 
            if (_doNotAccessDirectlyOutputPane is null)
            {
 
                var outputWindow = (IVsOutputWindow)_serviceProvider.GetService(typeof(SVsOutputWindow));
 
                // this should bring output window to the front
                _doNotAccessDirectlyOutputPane = CreateOutputPane(outputWindow);
            }
 
            return _doNotAccessDirectlyOutputPane!;
        }
 
        private IVsOutputWindowPane? CreateOutputPane(IVsOutputWindow outputWindow)
        {
            _threadingContext.AssertUIThread();
 
            // Try to get the workspace pane if it has already been registered
            var workspacePaneGuid = s_outputPaneGuid;
 
            // If the pane has already been created, CreatePane returns it
            if (ErrorHandler.Succeeded(outputWindow.CreatePane(ref workspacePaneGuid, "Razor Logger Output", fInitVisible: 1, fClearWithSolution: 1)) &&
                ErrorHandler.Succeeded(outputWindow.GetPane(ref workspacePaneGuid, out var pane)))
            {
                return pane;
            }
 
            return null;
        }
 
        public void Dispose()
        {
            _outputQueue.Complete();
            _disposalTokenSource.Cancel();
        }
    }
 
    private class Scope : IDisposable
    {
        public static readonly Scope Instance = new();
 
        public void Dispose()
        {
        }
    }
}