|
// 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.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback;
using Newtonsoft.Json.Linq;
using Task = System.Threading.Tasks.Task;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.EditAndContinue;
namespace Microsoft.VisualStudio.LanguageServices.EditAndContinue;
[Export(typeof(IFeedbackDiagnosticFileProvider))]
internal sealed class EditAndContinueFeedbackDiagnosticFileProvider : IFeedbackDiagnosticFileProvider
{
/// <summary>
/// Name of the file displayed in VS Feedback UI.
/// See https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1714452.
/// </summary>
private const string ZipFileName = "source_files_and_binaries_updated_during_hot_reload.zip";
private const string VSFeedbackSemaphoreDir = @"Microsoft\VSFeedbackCollector";
private const string VSFeedbackSemaphoreFileName = "feedback.recording.json";
/// <summary>
/// VS Feedback creates a JSON file at the start of feedback session and deletes it when the session is over.
/// Watching the file is currently the only way to detect the feedback session.
/// </summary>
private readonly string _vsFeedbackSemaphoreFullPath;
private readonly FileSystemWatcher? _vsFeedbackSemaphoreFileWatcher;
private readonly int _vsProcessId;
private readonly DateTime _vsProcessStartTime;
private readonly string _tempDir;
private volatile int _isLogCollectionInProgress;
private readonly Lazy<EditAndContinueLanguageService>? _encService;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public EditAndContinueFeedbackDiagnosticFileProvider(
[Import(AllowDefault = true)] Lazy<EditAndContinueLanguageService>? encService = null)
{
_encService = encService;
var vsProcess = Process.GetCurrentProcess();
_vsProcessId = vsProcess.Id;
_vsProcessStartTime = vsProcess.StartTime;
_tempDir = Path.GetTempPath();
var vsFeedbackTempDir = Path.Combine(_tempDir, VSFeedbackSemaphoreDir);
_vsFeedbackSemaphoreFullPath = Path.Combine(vsFeedbackTempDir, VSFeedbackSemaphoreFileName);
// Directory may not exist in scenarios such as Razor integration tests
if (!Directory.Exists(vsFeedbackTempDir))
{
return;
}
_vsFeedbackSemaphoreFileWatcher = new FileSystemWatcher(vsFeedbackTempDir, VSFeedbackSemaphoreFileName);
_vsFeedbackSemaphoreFileWatcher.Created += (_, _) => OnFeedbackSemaphoreCreatedOrChanged();
_vsFeedbackSemaphoreFileWatcher.Changed += (_, _) => OnFeedbackSemaphoreCreatedOrChanged();
_vsFeedbackSemaphoreFileWatcher.Deleted += (_, _) => OnFeedbackSemaphoreDeleted();
if (File.Exists(_vsFeedbackSemaphoreFullPath))
{
OnFeedbackSemaphoreCreatedOrChanged();
}
_vsFeedbackSemaphoreFileWatcher.EnableRaisingEvents = true;
}
/// <summary>
/// Reuse the same directory for multiple feedback sessions originating from the same VS instance.
/// Log files for different debugging sessions will be in separate subdirectories so they will not collide,
/// but the later feedback sessions will include all files logged for the previous sessions as well.
/// Also if the compression and/or uploading of the zip file is not finished by the time the new recording starts
/// we might not be able to write the new zip file to disk and the previous content might be uploaded instead.
/// See https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1716980
/// </summary>
private string GetLogDirectory()
=> Path.Combine(Path.Combine(_tempDir, $"EnC_{_vsProcessId}", "Log"));
private string GetZipFilePath()
=> Path.Combine(Path.Combine(_tempDir, $"EnC_{_vsProcessId}", ZipFileName));
public IReadOnlyCollection<string> GetFiles()
=> _vsFeedbackSemaphoreFileWatcher is null
? Array.Empty<string>()
: (IReadOnlyCollection<string>)([GetZipFilePath()]);
private void OnFeedbackSemaphoreCreatedOrChanged()
{
if (!IsLoggingEnabledForCurrentVisualStudioInstance(_vsFeedbackSemaphoreFullPath))
{
// The semaphore file was created for another VS instance.
return;
}
if (Interlocked.CompareExchange(ref _isLogCollectionInProgress, 1, 0) == 0)
{
_encService?.Value.SetFileLoggingDirectory(GetLogDirectory());
}
}
private void OnFeedbackSemaphoreDeleted()
{
if (Interlocked.Exchange(ref _isLogCollectionInProgress, 0) == 1)
{
_encService?.Value.SetFileLoggingDirectory(logDirectory: null);
// Including the zip files in VS Feedback is currently on best effort basis.
// See https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1714439
Task.Run(() =>
{
try
{
ZipFile.CreateFromDirectory(GetLogDirectory(), GetZipFilePath());
}
catch
{
}
});
}
}
private bool IsLoggingEnabledForCurrentVisualStudioInstance(string semaphoreFilePath)
{
try
{
if (_vsProcessStartTime > File.GetCreationTime(semaphoreFilePath))
{
// Semaphore file is older than the running instance of VS
return false;
}
// Check the contents of the semaphore file to see if it's for this instance of VS
var content = File.ReadAllText(semaphoreFilePath);
return JObject.Parse(content)["processIds"] is JContainer pidCollection && pidCollection.Values<int>().Contains(_vsProcessId);
}
catch
{
// Something went wrong opening or parsing the semaphore file - ignore it
return false;
}
}
}
|