File: VisualStudioLogging.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.VisualStudio.Razor.IntegrationTests\Microsoft.VisualStudio.Razor.IntegrationTests.csproj (Microsoft.VisualStudio.Razor.IntegrationTests)
// 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;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell;
using Xunit;
using Xunit.Harness;
 
namespace Microsoft.VisualStudio.Razor.IntegrationTests;
 
internal static class VisualStudioLogging
{
    private static bool s_customLoggersAdded = false;
 
    public const string RazorOutputLogId = "RazorOutputLog";
    public const string LogHubLogId = "RazorLogHub";
    public const string ServiceHubLogId = "ServiceHubLog";
    public const string ComponentModelCacheId = "ComponentModelCache";
    public const string ExtensionDirectoryId = "ExtensionDirectory";
    public const string MEFErrorId = "MEFErrorsFromHive";
 
    private static readonly object s_lockObj = new();
 
    public static void AddCustomLoggers()
    {
        lock (s_lockObj)
        {
            // Add custom logs on failure if they haven't already been.
            if (!s_customLoggersAdded)
            {
                DataCollectionService.RegisterCustomLogger(RazorOutputPaneLogger, RazorOutputLogId, "log");
                DataCollectionService.RegisterCustomLogger(RazorLogHubLogger, LogHubLogId, "zip");
                DataCollectionService.RegisterCustomLogger(RazorServiceHubLogger, ServiceHubLogId, "zip");
                DataCollectionService.RegisterCustomLogger(RazorComponentModelCacheLogger, ComponentModelCacheId, "zip");
                DataCollectionService.RegisterCustomLogger(RazorExtensionExplorerLogger, ExtensionDirectoryId, "txt");
                DataCollectionService.RegisterCustomLogger(RazorMEFErrorLogger, MEFErrorId, "txt");
 
                s_customLoggersAdded = true;
            }
        }
    }
 
    private static void RazorMEFErrorLogger(string filePath)
    {
        var hiveDirectory = GetHiveDirectory();
        var errorFile = Path.Combine(hiveDirectory, "ComponentModelCache", "Microsoft.VisualStudio.Default.err");
        if (File.Exists(errorFile))
        {
            File.Copy(errorFile, filePath);
        }
    }
 
    private static void RazorLogHubLogger(string filePath)
    {
        FeedbackLoggerInternal(filePath, "LogHub", "Razor");
    }
 
    private static void RazorServiceHubLogger(string filePath)
    {
        FeedbackLoggerInternal(filePath, "ServiceHubLogs");
    }
 
    private static void RazorComponentModelCacheLogger(string filePath)
    {
        FeedbackLoggerInternal(filePath, "ComponentModelCache");
    }
 
    private static void FeedbackLoggerInternal(string filePath, params string[] expectedFileParts)
    {
        var componentModel = GlobalServiceProvider.ServiceProvider.GetService<SComponentModel, IComponentModel>();
        if (componentModel is null)
        {
            // Unable to get componentModel
            return;
        }
 
        var feedbackFileProviders = componentModel.GetExtensions<IFeedbackDiagnosticFileProvider>();
 
        // Collect all the file names first since they can kick of file creation events that might need extra time to resolve.
        var files = new List<string>();
        foreach (var feedbackFileProvider in feedbackFileProviders)
        {
            try
            {
                files.AddRange(feedbackFileProvider.GetFiles());
            }
            catch
            {
                // If one of the providers has issues, we don't want it causing us to not be able to report our stuff properly
            }
        }
 
        _ = CollectFeedbackItemsAsync(files, filePath, expectedFileParts);
    }
 
    private static void RazorExtensionExplorerLogger(string filePath)
    {
        var hiveDirectory = GetHiveDirectory();
 
        using var _ = StringBuilderPool.GetPooledObject(out var fileBuilder);
 
        var extensionsDir = Path.Combine(hiveDirectory, "Extensions");
        var compatListFile = Path.Combine(extensionsDir, "CompatibilityList.xml");
        if (File.Exists(compatListFile))
        {
            var compatListContent = File.ReadAllText(compatListFile);
            fileBuilder.AppendLine("CompatListContents:");
            fileBuilder.AppendLine(compatListContent);
        }
        else
        {
            fileBuilder.AppendLine("Missing CompatList file");
        }
 
        var microsoftDir = Path.Combine(extensionsDir, "Microsoft");
        var msExtensionFiles = Directory.EnumerateFiles(microsoftDir, "*", SearchOption.AllDirectories);
        foreach (var msExtensionFile in msExtensionFiles)
        {
            fileBuilder.Append("  ");
            fileBuilder.AppendLine(msExtensionFile);
        }
 
        File.WriteAllText(filePath, fileBuilder.ToString());
    }
 
    internal static string GetHiveDirectory()
    {
        // There could be multiple copies of visual studio installed, each with their own RoslynDev hive
        // so to make sure we find the one for the instance of VS we are actually running, we need to find
        // the installation ID for this install. This is stored in an ini file, next to devenv.exe, and the
        // ID itself is pre-pended to the hive name in the file system.
        var devenvPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
        var isolationIni = Path.Combine(devenvPath, "devenv.isolation.ini");
 
        var installationId = "";
        // Lazy ini file parsing starts now!
        foreach (var line in File.ReadAllLines(isolationIni))
        {
            if (line.StartsWith("InstallationID=", StringComparison.OrdinalIgnoreCase))
            {
                installationId = line.Split('=')[1];
                break;
            }
        }
 
        var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var vsLocalDir = Path.Combine(localAppData, "Microsoft", "VisualStudio");
 
        // Just in case the enterprise grade ini file parsing above didn't work, or VS changes how they
        // store things, the following is written to work even if installationId is an empty string. In
        // that case it will fall back to the previous behavior of expecting a single RoslynDev hive to
        // exist, or fail.
        var directories = Directory.GetDirectories(vsLocalDir, $"18*{installationId}RoslynDev", SearchOption.TopDirectoryOnly);
        var hiveDirectories = directories.Where(d => !d.Contains("$")).ToList();
        if (hiveDirectories.Count == 0)
        {
            directories = Directory.GetDirectories(vsLocalDir, $"17*{installationId}RoslynDev", SearchOption.TopDirectoryOnly);
            hiveDirectories = directories.Where(d => !d.Contains("$")).ToList();
        }
 
        Assert.True(hiveDirectories.Count == 1, $"Could not find the hive path for InstallationID '{installationId}'. Found instead:{Environment.NewLine}{string.Join(Environment.NewLine, hiveDirectories)}");
 
        return hiveDirectories[0];
    }
 
    private static void RazorOutputPaneLogger(string filePath)
    {
        // JoinableTaskFactory.Run isn't an option because we might be disposing already.
        // Don't use ThreadHelper.JoinableTaskFactory in test methods, but it's correct here.
#pragma warning disable VSTHRD103 // Call async methods when in an async method
        ThreadHelper.JoinableTaskFactory.Run(async () =>
#pragma warning restore VSTHRD103 // Call async methods when in an async method
        {
            try
            {
                var testServices = await Extensibility.Testing.TestServices.CreateAsync(ThreadHelper.JoinableTaskFactory);
                var paneContent = await testServices.Output.GetRazorOutputPaneContentAsync(CancellationToken.None);
                File.WriteAllText(filePath, paneContent);
            }
            catch (Exception)
            {
                // Eat any errors so we don't block further collection
            }
        });
    }
 
    private static async Task CollectFeedbackItemsAsync(IEnumerable<string> files, string destination, string[] expectedFileParts)
    {
        // What's important in this weird threading stuff is ensuring we vacate the thread RazorLogHubLogger was called on
        // because if we don't it ends up blocking the thread that creates the zip file we need.
        await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
        {
            foreach (var file in files)
            {
                var name = Path.GetFileName(file);
 
                if (!Path.GetExtension(file).Equals(".zip", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
 
                if (expectedFileParts.All(part => name.IndexOf(part, StringComparison.OrdinalIgnoreCase) == -1))
                {
                    continue;
                }
 
                await Task.Run(() =>
                {
                    WaitForFileExists(file);
                    WaitForFileFinishedWriting(file);
                    if (File.Exists(file))
                    {
                        File.Copy(file, destination);
                    }
                });
            }
        });
    }
 
    private static void WaitForFileExists(string file)
    {
        const int MaxRetries = 50;
        var retries = 0;
        while (!File.Exists(file) && retries < MaxRetries)
        {
            retries++;
            // Free your thread
            Thread.Yield();
            // Wait a bit
            Thread.Sleep(100);
        }
    }
 
    private static void WaitForFileFinishedWriting(string file)
    {
        const int MaxRetries = 50;
        var retries = 0;
        while (IsFileLocked(file) && retries < MaxRetries)
        {
            retries++;
            // Free your thread
            Thread.Yield();
            // Wait a bit
            Thread.Sleep(100);
        }
    }
 
    private static bool IsFileLocked(string file)
    {
        FileStream? stream = null;
 
        try
        {
            stream = File.Open(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
        }
        catch (IOException)
        {
            //the file is unavailable because it is:
            //still being written to
            //or being processed by another thread
            //or does not exist (has already been processed)
            return true;
        }
        finally
        {
            if (stream != null)
                stream.Close();
        }
 
        //file is not locked
        return false;
    }
}