Skip to content

Commit

Permalink
Merge pull request #73809 from dibarbet/fix_analyzer_loading_linux
Browse files Browse the repository at this point in the history
Switch to workspace service to ensure the analyzer loader uses streams on linux
  • Loading branch information
dibarbet authored Jun 7, 2024
2 parents a047928 + 6dd6aac commit f0a5241
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;

namespace Microsoft.CodeAnalysis.Workspaces;

[ExportWorkspaceService(typeof(IAnalyzerAssemblyLoaderProvider), [WorkspaceKind.Host]), Shared]
internal class VSAnalyzerAssemblyLoaderProvider : AbstractAnalyzerAssemblyLoaderProvider
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VSAnalyzerAssemblyLoaderProvider([ImportMany] IEnumerable<IAnalyzerAssemblyResolver> externalResolvers) : base(externalResolvers)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public class WorkspaceProjectFactoryServiceTests
[Fact]
public async Task CreateProjectAndBatch()
{
var loggerFactory = new LoggerFactory();
using var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
new LoggerFactory(), includeDevKitComponents: false, out var serverConfiguration, out var _);
loggerFactory, includeDevKitComponents: false, out var serverConfiguration, out var _);

exportProvider.GetExportedValue<ServerConfigurationFactory>()
.InitializeConfiguration(serverConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
// Immediately set the logger factory, so that way it'll be available for the rest of the composition
exportProvider.GetExportedValue<ServerLoggerFactory>().SetFactory(loggerFactory);

// Also add the ExtensionAssemblyManager so it will be available for the rest of the composition.
exportProvider.GetExportedValue<ExtensionAssemblyManagerMefProvider>().SetMefExtensionAssemblyManager(extensionManager);

return exportProvider;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal sealed class LanguageServerWorkspaceFactory
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public LanguageServerWorkspaceFactory(
HostServicesProvider hostServicesProvider,
VSCodeAnalyzerLoader analyzerLoader,
IFileChangeWatcher fileChangeWatcher,
[ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, Host.Mef.FileExtensionsMetadata>> dynamicFileInfoProviders,
ProjectTargetFrameworkManager projectTargetFrameworkManager,
Expand All @@ -39,8 +38,6 @@ public LanguageServerWorkspaceFactory(
ProjectSystemProjectFactory = new ProjectSystemProjectFactory(Workspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { });
workspace.ProjectSystemProjectFactory = ProjectSystemProjectFactory;

analyzerLoader.InitializeDiagnosticsServices();

var razorSourceGenerator = serverConfigurationFactory?.ServerConfiguration?.RazorSourceGenerator;
ProjectSystemHostInfo = new ProjectSystemHostInfo(
DynamicFileInfoProviders: dynamicFileInfoProviders.ToImmutableArray(),
Expand All @@ -56,10 +53,10 @@ public LanguageServerWorkspaceFactory(
public ProjectSystemHostInfo ProjectSystemHostInfo { get; }
public ProjectTargetFrameworkManager TargetFrameworkManager { get; }

public async Task InitializeSolutionLevelAnalyzersAsync(ImmutableArray<string> analyzerPaths, ExtensionAssemblyManager extensionAssemblyManager)
public async Task InitializeSolutionLevelAnalyzersAsync(ImmutableArray<string> analyzerPaths)
{
var references = new List<AnalyzerFileReference>();
var analyzerLoader = VSCodeAnalyzerLoader.CreateAnalyzerAssemblyLoader(extensionAssemblyManager, _logger);
var analyzerLoader = Workspace.Services.GetRequiredService<IAnalyzerAssemblyLoaderProvider>().GetLoader(shadowCopy: true);

foreach (var analyzerPath in analyzerPaths)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,47 @@
// 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.Collections.Immutable;
using System.Composition;
using System.Reflection;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Services;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

[Export(typeof(VSCodeAnalyzerLoader)), Shared]
internal class VSCodeAnalyzerLoader
[ExportWorkspaceService(typeof(IAnalyzerAssemblyLoaderProvider), [WorkspaceKind.Host]), Shared]
internal class VSCodeAnalyzerLoaderProvider : AbstractAnalyzerAssemblyLoaderProvider
{
private readonly ExtensionAssemblyManager _extensionAssemblyManager;
private readonly ILoggerFactory _loggerFactory;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VSCodeAnalyzerLoader()
public VSCodeAnalyzerLoaderProvider(
ExtensionAssemblyManager extensionAssemblyManager,
ILoggerFactory loggerFactory,
[ImportMany] IEnumerable<IAnalyzerAssemblyResolver> externalResolvers) : base(externalResolvers)
{
_extensionAssemblyManager = extensionAssemblyManager;
_loggerFactory = loggerFactory;
}

#pragma warning disable CA1822 // Mark members as static
public void InitializeDiagnosticsServices()
#pragma warning restore CA1822 // Mark members as static
protected override IAnalyzerAssemblyLoader CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
{
}

public static IAnalyzerAssemblyLoader CreateAnalyzerAssemblyLoader(ExtensionAssemblyManager extensionAssemblyManager, ILogger logger)
{
return new VSCodeExtensionAssemblyAnalyzerLoader(extensionAssemblyManager, logger);
var baseLoader = base.CreateShadowCopyLoader(externalResolvers);
return new VSCodeExtensionAssemblyAnalyzerLoader(baseLoader, _extensionAssemblyManager, _loggerFactory.CreateLogger<VSCodeExtensionAssemblyAnalyzerLoader>());
}

/// <summary>
/// Analyzer loader that will re-use already loaded assemblies from the extension load context.
/// </summary>
private class VSCodeExtensionAssemblyAnalyzerLoader(ExtensionAssemblyManager extensionAssemblyManager, ILogger logger) : IAnalyzerAssemblyLoader
private class VSCodeExtensionAssemblyAnalyzerLoader(IAnalyzerAssemblyLoader defaultLoader, ExtensionAssemblyManager extensionAssemblyManager, ILogger logger) : IAnalyzerAssemblyLoader
{
private readonly DefaultAnalyzerAssemblyLoader _defaultLoader = new();

public void AddDependencyLocation(string fullPath)
{
_defaultLoader.AddDependencyLocation(fullPath);
defaultLoader.AddDependencyLocation(fullPath);
}

public Assembly LoadFromPath(string fullPath)
Expand All @@ -51,7 +54,7 @@ public Assembly LoadFromPath(string fullPath)
return assembly;
}

return _defaultLoader.LoadFromPath(fullPath);
return defaultLoader.LoadFromPath(fullPath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation
// Include analyzers from extension assemblies.
analyzerPaths = analyzerPaths.AddRange(extensionManager.ExtensionAssemblyPaths);

await workspaceFactory.InitializeSolutionLevelAnalyzersAsync(analyzerPaths, extensionManager);
await workspaceFactory.InitializeSolutionLevelAnalyzersAsync(analyzerPaths);

var serviceBrokerFactory = exportProvider.GetExportedValue<ServiceBrokerFactory>();
StarredCompletionAssemblyHelper.InitializeInstance(serverConfiguration.StarredCompletionsPath, extensionManager, loggerFactory, serviceBrokerFactory);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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.Composition;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.LanguageServer.Services;

/// <summary>
/// Provider to allow MEF access to <see cref="ExtensionAssemblyManager"/>
/// Must be done this way as the manager is required to create MEF as well.
/// </summary>
[Export, Shared]
internal class ExtensionAssemblyManagerMefProvider
{
private ExtensionAssemblyManager? _extensionAssemblyManager;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public ExtensionAssemblyManagerMefProvider()
{
}

[Export]
public ExtensionAssemblyManager ExtensionAssemblyManager => _extensionAssemblyManager ?? throw new InvalidOperationException($"{nameof(ExtensionAssemblyManager)} is not initialized");

public void SetMefExtensionAssemblyManager(ExtensionAssemblyManager extensionAssemblyManager)
{
_extensionAssemblyManager = extensionAssemblyManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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.Composition;
using System.IO;

namespace Microsoft.CodeAnalysis.Host;

/// <summary>
/// Abstract implementation of an anlyzer assembly loader that can be used by VS/VSCode to provide a
/// <see cref="IAnalyzerAssemblyLoader"/> with an appropriate path.
/// </summary>
internal abstract class AbstractAnalyzerAssemblyLoaderProvider : IAnalyzerAssemblyLoaderProvider
{
private readonly DefaultAnalyzerAssemblyLoader _loader;
private readonly Lazy<IAnalyzerAssemblyLoader> _shadowCopyLoader;

public AbstractAnalyzerAssemblyLoaderProvider([ImportMany] IEnumerable<IAnalyzerAssemblyResolver> externalResolvers)
{
var resolvers = externalResolvers.ToImmutableArray();
_loader = new(resolvers);

// We use a lazy here in case creating the loader requires MEF imports in the derived constructor.
_shadowCopyLoader = new Lazy<IAnalyzerAssemblyLoader>(() => CreateShadowCopyLoader(resolvers));
}

public IAnalyzerAssemblyLoader GetLoader(bool shadowCopy)
=> shadowCopy ? _shadowCopyLoader.Value : _loader;

protected virtual IAnalyzerAssemblyLoader CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
{
return DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(
Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader"),
externalResolvers: externalResolvers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.Editor.UnitTests
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Options
Imports Microsoft.CodeAnalysis.Test.Utilities
Imports Microsoft.CodeAnalysis.Workspaces.ProjectSystem
Expand All @@ -26,7 +27,9 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
<WpfFact>
Public Sub GetReferenceCalledMultipleTimes()
Using workspace = New TestWorkspace(composition:=s_compositionWithMockDiagnosticUpdateSourceRegistrationService)
Using tempRoot = New TempRoot(), analyzer = New ProjectAnalyzerReference(tempRoot.CreateFile().Path, HostDiagnosticUpdateSource.Instance, ProjectId.CreateNewId(), LanguageNames.VisualBasic)
Dim provider = workspace.Services.GetRequiredService(Of IAnalyzerAssemblyLoaderProvider)
Dim service = provider.GetLoader(shadowCopy:=True)
Using tempRoot = New TempRoot(), analyzer = New ProjectAnalyzerReference(tempRoot.CreateFile().Path, service, HostDiagnosticUpdateSource.Instance, ProjectId.CreateNewId(), LanguageNames.VisualBasic)
Dim reference1 = analyzer.GetReference()
Dim reference2 = analyzer.GetReference()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private sealed class DefaultAnalyzerAssemblyLoaderProvider(string workspaceKind,
Path.Combine(Path.GetTempPath(), "CodeAnalysis", "WorkspacesAnalyzerShadowCopies", workspaceKind),
externalResolvers: externalResolvers);

public IAnalyzerAssemblyLoader GetLoader(in AnalyzerAssemblyLoaderOptions options)
=> options.ShadowCopy ? _shadowCopyLoader : _loader;
public IAnalyzerAssemblyLoader GetLoader(bool shadowCopy)
=> shadowCopy ? _shadowCopyLoader : _loader;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public virtual AnalyzerReference ReadAnalyzerReferenceFrom(ObjectReader reader,
case nameof(AnalyzerFileReference):
var fullPath = reader.ReadRequiredString();
var shadowCopy = reader.ReadBoolean();
return new AnalyzerFileReference(fullPath, _analyzerLoaderProvider.GetLoader(new AnalyzerAssemblyLoaderOptions(shadowCopy)));
return new AnalyzerFileReference(fullPath, _analyzerLoaderProvider.GetLoader(shadowCopy));

case nameof(AnalyzerImageReference):
var guid = reader.ReadGuid();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ namespace Microsoft.CodeAnalysis.Host;

internal interface IAnalyzerAssemblyLoaderProvider : IWorkspaceService
{
IAnalyzerAssemblyLoader GetLoader(in AnalyzerAssemblyLoaderOptions options);
IAnalyzerAssemblyLoader GetLoader(bool shadowCopy);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal sealed partial class ProjectSystemProject

private readonly ProjectSystemProjectFactory _projectSystemProjectFactory;
private readonly ProjectSystemHostInfo _hostInfo;
private readonly IAnalyzerAssemblyLoader _analyzerAssemblyLoader;

/// <summary>
/// A semaphore taken for all mutation of any mutable field in this type.
Expand Down Expand Up @@ -156,6 +157,12 @@ internal ProjectSystemProject(
Language = language;
_displayName = displayName;

var provider = _projectSystemProjectFactory.Workspace.Services.GetRequiredService<IAnalyzerAssemblyLoaderProvider>();
// Shadow copy analyzer files coming from packages to avoid locking the files in NuGet cache.
// NOTE: The provider will always return the same singleton analyzer loader instance, which is important to
// ensure that shadow copied analyzer dependencies are correctly loaded.
_analyzerAssemblyLoader = provider.GetLoader(shadowCopy: true);

_sourceFiles = new BatchingDocumentCollection(
this,
documentAlreadyInWorkspace: (s, d) => s.ContainsDocument(d),
Expand Down Expand Up @@ -918,6 +925,7 @@ public void AddAnalyzerReference(string fullPath)
// Nope, we actually need to make a new one.
var visualStudioAnalyzer = new ProjectAnalyzerReference(
mappedFullPath,
_analyzerAssemblyLoader,
_hostInfo.DiagnosticSource,
Id,
Language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;

// TODO: Remove. This is only needed to support Solution Explorer Analyzer node population.
// Analyzers should not be loaded in devenv process (see https://github.com/dotnet/roslyn/issues/43008).
internal sealed class ProjectAnalyzerReference(string fullPath, IProjectSystemDiagnosticSource projectSystemDiagnosticSource, ProjectId projectId, string language) : IDisposable
internal sealed class ProjectAnalyzerReference(string fullPath, IAnalyzerAssemblyLoader loader, IProjectSystemDiagnosticSource projectSystemDiagnosticSource, ProjectId projectId, string language) : IDisposable
{
// Shadow copy analyzer files coming from packages to avoid locking the files in NuGet cache.
// NOTE: It is important that we share the same shadow copy assembly loader for all VisualStudioAnalyzer instances.
// This is required to ensure that shadow copied analyzer dependencies are correctly loaded.
private static readonly IAnalyzerAssemblyLoader s_analyzerAssemblyLoader =
new ShadowCopyAnalyzerAssemblyLoader(Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader"));

// these 2 are mutable states that must be guarded under the _gate.
private readonly object _gate = new();
private AnalyzerReference? _analyzerReference;
Expand All @@ -35,7 +29,7 @@ public AnalyzerReference GetReference()
// TODO: ensure the file watcher is subscribed
// (tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/661546)

var analyzerFileReference = new AnalyzerFileReference(FullPath, s_analyzerAssemblyLoader);
var analyzerFileReference = new AnalyzerFileReference(FullPath, loader);
analyzerFileReference.AnalyzerLoadFailed += OnAnalyzerLoadError;
_analyzerReference = analyzerFileReference;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public RemoteAnalyzerAssemblyLoaderService([ImportMany] IEnumerable<IAnalyzerAss
_shadowCopyLoader = new(Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader"), resolvers);
}

public IAnalyzerAssemblyLoader GetLoader(in AnalyzerAssemblyLoaderOptions options)
=> options.ShadowCopy ? _shadowCopyLoader : _loader;
public IAnalyzerAssemblyLoader GetLoader(bool shadowCopy)
=> shadowCopy ? _shadowCopyLoader : _loader;
}
}

0 comments on commit f0a5241

Please sign in to comment.