Skip to content

Commit

Permalink
Merge pull request #73360 from CyrusNajmabadi/masShutdown
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored May 7, 2024
2 parents 973a9e1 + 60ccf68 commit 31bb53c
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ protected static async Task GenerateFileAndVerifyAsync(
}
finally
{
service.CleanupGeneratedFiles();
service.TryGetWorkspace()?.Dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,6 @@ await RunTestAsync(async path =>
}
finally
{
service.CleanupGeneratedFiles();
service.TryGetWorkspace()?.Dispose();
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,7 @@ public static void VerifyDocumentNotReused(MetadataAsSourceFile a, MetadataAsSou

public void Dispose()
{
try
{
_metadataAsSourceService.CleanupGeneratedFiles();
}
finally
{
Workspace.Dispose();
}
Workspace.Dispose();
}

public async Task<ISymbol?> ResolveSymbolAsync(string symbolMetadataName, Compilation? compilation = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ internal interface IMetadataAsSourceFileService

bool TryRemoveDocumentFromWorkspace(string filePath);

void CleanupGeneratedFiles();

bool IsNavigableMetadataSymbol(ISymbol symbol);

Workspace? TryGetWorkspace();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,49 @@
namespace Microsoft.CodeAnalysis.MetadataAsSource;

[Export(typeof(IMetadataAsSourceFileService)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class MetadataAsSourceFileService([ImportMany] IEnumerable<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>> providers) : IMetadataAsSourceFileService
internal sealed class MetadataAsSourceFileService : IMetadataAsSourceFileService
{
private const string MetadataAsSource = nameof(MetadataAsSource);

/// <summary>
/// Set of providers that can be used to generate source for a symbol (for example, by decompiling, or by
/// extracting it from a pdb).
/// </summary>
private readonly ImmutableArray<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>> _providers = [.. ExtensionOrderer.Order(providers)];
private readonly Lazy<ImmutableArray<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>>> _providers;

/// <summary>
/// Workspace created the first time we generate any metadata for any symbol.
/// </summary>
private MetadataAsSourceWorkspace? _workspace;

/// <summary>
/// A lock to guard the mutex and filesystem data below. We want to ensure we generate into that and clean that
/// up safely.
/// A lock to ensure we initialize <see cref="_workspace"/> and cleanup stale data only once.
/// </summary>
private readonly SemaphoreSlim _gate = new(initialCount: 1);

/// <summary>
/// We create a mutex so other processes can see if our directory is still alive. We destroy the mutex when
/// we purge our generated files.
/// We create a mutex so other processes can see if our directory is still alive. As long as we own the mutex, no
/// other VS instance will try to delete our _rootTemporaryPathWithGuid folder.
/// </summary>
private Mutex? _mutex;
private string? _rootTemporaryPathWithGuid;
private readonly string _rootTemporaryPath = Path.Combine(Path.GetTempPath(), "MetadataAsSource");

private static string CreateMutexName(string directoryName)
=> "MetadataAsSource-" + directoryName;

private string GetRootPathWithGuid_NoLock()
private readonly Mutex _mutex;
private readonly string _rootTemporaryPathWithGuid;
private readonly string _rootTemporaryPath = Path.Combine(Path.GetTempPath(), MetadataAsSource);

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public MetadataAsSourceFileService(
[ImportMany] IEnumerable<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>> providers)
{
if (_rootTemporaryPathWithGuid == null)
{
var guidString = Guid.NewGuid().ToString("N");
_rootTemporaryPathWithGuid = Path.Combine(_rootTemporaryPath, guidString);
_mutex = new Mutex(initiallyOwned: true, name: CreateMutexName(guidString));
}
_providers = new(() => [.. ExtensionOrderer.Order(providers)]);

return _rootTemporaryPathWithGuid;
var guidString = Guid.NewGuid().ToString("N");
_rootTemporaryPathWithGuid = Path.Combine(_rootTemporaryPath, guidString);
_mutex = new Mutex(initiallyOwned: true, name: CreateMutexName(guidString));
}

private static string CreateMutexName(string directoryName)
=> $"{MetadataAsSource}-{directoryName}";

public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(
Workspace sourceWorkspace,
Project sourceProject,
Expand All @@ -87,28 +86,74 @@ public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(

using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
_workspace ??= new MetadataAsSourceWorkspace(this, sourceWorkspace.Services.HostServices);
if (_workspace is null)
{
_workspace = new MetadataAsSourceWorkspace(this, sourceWorkspace.Services.HostServices);

// We're being initialized the first time. Use this time to clean up any stale metadata-as-source files
// from previous VS sessions.
CleanupGeneratedFiles(_rootTemporaryPath);
}

Contract.ThrowIfNull(_workspace);
var tempPath = GetRootPathWithGuid_NoLock();

// We don't want to track telemetry for signatures only requests, only where we try to show source
using var telemetryMessage = signaturesOnly ? null : new TelemetryMessage(cancellationToken);

foreach (var lazyProvider in _providers)
foreach (var lazyProvider in _providers.Value)
{
var provider = lazyProvider.Value;
var providerTempPath = Path.Combine(tempPath, provider.GetType().Name);
var providerTempPath = Path.Combine(_rootTemporaryPathWithGuid, provider.GetType().Name);
var result = await provider.GetGeneratedFileAsync(_workspace, sourceWorkspace, sourceProject, symbol, signaturesOnly, options, providerTempPath, telemetryMessage, cancellationToken).ConfigureAwait(false);
if (result is not null)
{
return result;
}
}
}

// The decompilation provider can always return something
throw ExceptionUtilities.Unreachable();

static void CleanupGeneratedFiles(string rootDirectory)
{
try
{
if (Directory.Exists(rootDirectory))
{
// Let's look through directories to delete.
foreach (var directoryInfo in new DirectoryInfo(rootDirectory).EnumerateDirectories())
{
// Is there a mutex for this one? If so, that means it's a folder open in another VS instance.
// We should leave it alone. If not, then it's a folder from a previous VS run. Delete that
// now.
if (Mutex.TryOpenExisting(CreateMutexName(directoryInfo.Name), out var acquiredMutex))
{
acquiredMutex.Dispose();
}
else
{
TryDeleteFolderWhichContainsReadOnlyFiles(directoryInfo.FullName);
}
}
}
}
catch (Exception)
{
}
}

static void TryDeleteFolderWhichContainsReadOnlyFiles(string directoryPath)
{
try
{
foreach (var fileInfo in new DirectoryInfo(directoryPath).EnumerateFiles("*", SearchOption.AllDirectories))
IOUtilities.PerformIO(() => fileInfo.IsReadOnly = false);

IOUtilities.PerformIO(() => Directory.Delete(directoryPath, recursive: true));
}
catch (Exception)
{
}
}
}

private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
Expand All @@ -127,7 +172,7 @@ public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourc
{
AssertIsMainThread(workspace);

foreach (var provider in _providers)
foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
continue;
Expand All @@ -150,7 +195,7 @@ public bool TryRemoveDocumentFromWorkspace(string filePath)
{
AssertIsMainThread(workspace);

foreach (var provider in _providers)
foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
continue;
Expand Down Expand Up @@ -186,7 +231,7 @@ public bool ShouldCollapseOnOpen(string? filePath, BlockStructureOptions blockSt

AssertIsMainThread(workspace);

foreach (var provider in _providers)
foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
continue;
Expand All @@ -205,7 +250,7 @@ public bool ShouldCollapseOnOpen(string? filePath, BlockStructureOptions blockSt
Project? project = null;
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
foreach (var provider in _providers)
foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
continue;
Expand All @@ -229,80 +274,6 @@ public bool ShouldCollapseOnOpen(string? filePath, BlockStructureOptions blockSt
return new SymbolMappingResult(project, resolutionResult.Symbol);
}

public void CleanupGeneratedFiles()
{
using (_gate.DisposableWait())
{
// Release our mutex to indicate we're no longer using our directory and reset state
if (_mutex != null)
{
_mutex.Dispose();
_mutex = null;
_rootTemporaryPathWithGuid = null;
}

// Only cleanup for providers that have actually generated a file. This keeps us from accidentally loading
// lazy providers on cleanup that weren't used
var workspace = _workspace;
if (workspace != null)
{
foreach (var provider in _providers)
{
if (!provider.IsValueCreated)
continue;

provider.Value.CleanupGeneratedFiles(workspace);
}
}

try
{
if (Directory.Exists(_rootTemporaryPath))
{
var deletedEverything = true;

// Let's look through directories to delete.
foreach (var directoryInfo in new DirectoryInfo(_rootTemporaryPath).EnumerateDirectories())
{
// Is there a mutex for this one?
if (Mutex.TryOpenExisting(CreateMutexName(directoryInfo.Name), out var acquiredMutex))
{
acquiredMutex.Dispose();
deletedEverything = false;
continue;
}

TryDeleteFolderWhichContainsReadOnlyFiles(directoryInfo.FullName);
}

if (deletedEverything)
{
Directory.Delete(_rootTemporaryPath);
}
}
}
catch (Exception)
{
}
}
}

private static void TryDeleteFolderWhichContainsReadOnlyFiles(string directoryPath)
{
try
{
foreach (var fileInfo in new DirectoryInfo(directoryPath).EnumerateFiles("*", SearchOption.AllDirectories))
{
fileInfo.IsReadOnly = false;
}

Directory.Delete(directoryPath, recursive: true);
}
catch (Exception)
{
}
}

public bool IsNavigableMetadataSymbol(ISymbol symbol)
{
symbol = symbol.OriginalDefinition;
Expand Down

This file was deleted.

2 changes: 0 additions & 2 deletions src/VisualStudio/Core/Def/RoslynPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,6 @@ protected override async Task LoadComponentsAsync(CancellationToken cancellation
await this.ComponentModel.GetService<VisualStudioDiagnosticListTableCommandHandler>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);
await this.ComponentModel.GetService<VisualStudioDiagnosticListSuppressionStateService>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);

await this.ComponentModel.GetService<VisualStudioMetadataAsSourceFileSupportService>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);

await this.ComponentModel.GetService<IVisualStudioDiagnosticAnalyzerService>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);
await this.ComponentModel.GetService<RemoveUnusedReferencesCommandHandler>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);
await this.ComponentModel.GetService<SyncNamespacesCommandHandler>().InitializeAsync(this, cancellationToken).ConfigureAwait(false);
Expand Down
Loading

0 comments on commit 31bb53c

Please sign in to comment.