Skip to content

Commit

Permalink
Fix analyzer conflicts on Linux
Browse files Browse the repository at this point in the history
This changes analyzer loading on Linux to be per user where before
clashes could occur in the temp directory.

This fix initially started out by making our temp directory logic more
robust. Essentially add per user sub directories, do extended permission
checks, etc ... That exacerbated a number of existing corner cases in
the code and forced us to put a lot more error handling into the
workspaces layer.

After discussion with the IDE team I decided to take the approach of
loading from a `Stream` on non-Windows platforms. The performance
difference is neglible (1ms to 2ms per assembly) and it removes the
overhead of copying them around on disk as well as the complexities that
come with managing the shadow copy directory.

This will be observable to any analyzer that used `Assembly.Location` as
it will now be `""`. That is only useful though for inspecting disks
which is already illegal for analyzers hence it's considered an
acceptable break.

closes dotnet#65415
  • Loading branch information
Jared Parsons committed Aug 4, 2023
1 parent 6338d83 commit 9760b22
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 114 deletions.
183 changes: 103 additions & 80 deletions src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace Microsoft.CodeAnalysis.UnitTests

public sealed class InvokeUtil
{
public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compilerContext, AssemblyLoadTestFixture fixture, bool shadowLoad, string typeName, string methodName)
public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compilerContext, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName)
{
// Ensure that the test did not load any of the test fixture assemblies into
// the default load context. That should never happen. Assemblies should either
Expand All @@ -46,9 +46,14 @@ public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compile
var compilerContextCount = compilerContext.Assemblies.Count();

using var tempRoot = new TempRoot();
AnalyzerAssemblyLoader loader = shadowLoad
? new ShadowCopyAnalyzerAssemblyLoader(compilerContext, tempRoot.CreateDirectory().Path)
: new DefaultAnalyzerAssemblyLoader(compilerContext);
AnalyzerAssemblyLoader loader = kind switch
{
AnalyzerTestKind.LoadDirect => new DefaultAnalyzerAssemblyLoader(compilerContext, AnalyzerLoadOption.LoadFromDisk),
AnalyzerTestKind.LoadStream => new DefaultAnalyzerAssemblyLoader(compilerContext, AnalyzerLoadOption.LoadFromStream),
AnalyzerTestKind.ShadowLoad => new ShadowCopyAnalyzerAssemblyLoader(compilerContext, tempRoot.CreateDirectory().Path),
_ => throw new Exception()
};

try
{
AnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName);
Expand Down Expand Up @@ -88,12 +93,15 @@ public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compile

public sealed class InvokeUtil : MarshalByRefObject
{
public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture fixture, bool shadowLoad, string typeName, string methodName)
public void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadTestFixture fixture, AnalyzerTestKind kind, string typeName, string methodName)
{
using var tempRoot = new TempRoot();
AnalyzerAssemblyLoader loader = shadowLoad
? new ShadowCopyAnalyzerAssemblyLoader(tempRoot.CreateDirectory().Path)
: new DefaultAnalyzerAssemblyLoader();
AnalyzerAssemblyLoader loader = kind switch
{
AnalyzerTestKind.LoadDirect => new DefaultAnalyzerAssemblyLoader(),
AnalyzerTestKind.ShadowLoad => new ShadowCopyAnalyzerAssemblyLoader(tempRoot.CreateDirectory().Path),
_ => throw new Exception()
};

try
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.

Expand All @@ -11,19 +11,46 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace Microsoft.CodeAnalysis
{
internal enum AnalyzerLoadOption
{
/// <summary>
/// Once the assembly path is chosen load it directly from disk at that location
/// </summary>
LoadFromDisk,

/// <summary>
/// Once the assembly path is chosen, read the contents of disk and load from memory
/// </summary>
/// <remarks>
/// While Windows supports this option it comes with a significant performance penalty do
/// to anti virus scans. It can have a load time of 300-500ms while loading from disk
/// is generally 1-2ms. Use this with caution on Windows.
/// </remarks>
LoadFromStream
}

internal partial class AnalyzerAssemblyLoader
{
private readonly AssemblyLoadContext _compilerLoadContext;
private readonly Dictionary<string, DirectoryLoadContext> _loadContextByDirectory = new Dictionary<string, DirectoryLoadContext>(StringComparer.Ordinal);
private readonly AnalyzerLoadOption _loadOption;

internal AssemblyLoadContext CompilerLoadContext => _compilerLoadContext;
internal AnalyzerLoadOption AnalyzerLoadOption => _loadOption;

internal AnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext = null)
internal AnalyzerAssemblyLoader()
: this(null, AnalyzerLoadOption.LoadFromDisk)
{
}

internal AnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext, AnalyzerLoadOption loadOption)
{
_loadOption = loadOption;
_compilerLoadContext = compilerLoadContext ?? AssemblyLoadContext.GetLoadContext(typeof(AnalyzerAssemblyLoader).GetTypeInfo().Assembly)!;
}

Expand Down Expand Up @@ -105,7 +132,7 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader, Ass
if (_loader.IsAnalyzerDependencyPath(assemblyPath))
{
(_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath);
return LoadFromAssemblyPath(loadPath);
return loadCore(loadPath);
}

// Next prefer registered dependencies from other directories. Ideally this would not
Expand All @@ -114,11 +141,24 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader, Ass
// https://github.com/dotnet/roslyn/issues/56442
if (_loader.GetBestPath(assemblyName) is string bestRealPath)
{
return LoadFromAssemblyPath(bestRealPath);
return loadCore(bestRealPath);
}

// No analyzer registered this dependency. Time to fail
return null;

Assembly loadCore(string assemblyPath)
{
if (_loader.AnalyzerLoadOption == AnalyzerLoadOption.LoadFromDisk)
{
return LoadFromAssemblyPath(assemblyPath);
}
else
{
using var stream = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return LoadFromStream(stream);
}
}
}

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
Expand All @@ -17,14 +18,19 @@ internal sealed class DefaultAnalyzerAssemblyLoader : AnalyzerAssemblyLoader
{
#if NETCOREAPP

// Called from a netstandard2.0 project, so need to ensure a parameterless constructor is available.
internal DefaultAnalyzerAssemblyLoader()
: this(null)
{
}

internal DefaultAnalyzerAssemblyLoader(System.Runtime.Loader.AssemblyLoadContext? compilerLoadContext = null)
: base(compilerLoadContext)
internal DefaultAnalyzerAssemblyLoader(System.Runtime.Loader.AssemblyLoadContext? compilerLoadContext = null, AnalyzerLoadOption loadOption = AnalyzerLoadOption.LoadFromDisk)
: base(compilerLoadContext, loadOption)
{
}

#else

internal DefaultAnalyzerAssemblyLoader()
{
}

Expand All @@ -36,5 +42,43 @@ internal DefaultAnalyzerAssemblyLoader(System.Runtime.Loader.AssemblyLoadContext
/// <param name="fullPath"></param>
/// <returns></returns>
protected override string PreparePathToLoad(string fullPath) => fullPath;

/// <summary>
/// Return an <see cref="IAnalyzerAssemblyLoader"/> which does not lock assemblies on disk that is
/// most appropriate for the current platform.
/// </summary>
/// <param name="subPath">In the case a directory must be created on disk for shadow loading this
/// is the suffix added to that path</param>
/// <returns></returns>
internal static IAnalyzerAssemblyLoader CreateNonLockingLoader(string? subPath = null)
{
#if NETCOREAPP
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// The cost of doing stream based loading on Windows is too expensive and we must continue to
// use the shadow copy loader.
return createShadowLoaderWindows();
}
else
{
return new DefaultAnalyzerAssemblyLoader(loadOption: AnalyzerLoadOption.LoadFromStream);
}
#else
return createShadowLoaderWindows();
#endif

ShadowCopyAnalyzerAssemblyLoader createShadowLoaderWindows()
{
// The shadow copy analyzer should only be created on Windows. To create on Linux we cannot use
// GetTempPath as it's not per-user. Generally there is no need as LoadFromStream achieves the same
// effect
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
subPath ??= Path.Combine("CodeAnalysis", "AnalyzerShadowCopies");
var baseDirectory = Path.IsPathRooted(subPath)
? subPath
: Path.Combine(Path.GetTempPath(), subPath);
return new ShadowCopyAnalyzerAssemblyLoader(baseDirectory);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,23 @@ internal sealed class ShadowCopyAnalyzerAssemblyLoader : AnalyzerAssemblyLoader
internal int CopyCount => _assemblyDirectoryId;

#if NETCOREAPP
public ShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null)
public ShadowCopyAnalyzerAssemblyLoader(string baseDirectory)
: this(null, baseDirectory)
{
}

public ShadowCopyAnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext, string? baseDirectory = null)
: base(compilerLoadContext)
public ShadowCopyAnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext, string baseDirectory)
: base(compilerLoadContext, AnalyzerLoadOption.LoadFromDisk)
#else
public ShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null)
public ShadowCopyAnalyzerAssemblyLoader(string baseDirectory)
#endif
{
if (baseDirectory != null)
if (baseDirectory is null)
{
_baseDirectory = baseDirectory;
}
else
{
// https://github.com/dotnet/roslyn/issues/65415
// Fixing that issue will involve removing this GetTempPath call
#pragma warning disable RS0030
_baseDirectory = Path.Combine(Path.GetTempPath(), "CodeAnalysis", "AnalyzerShadowCopies");
#pragma warning restore RS0030
throw new ArgumentNullException(nameof(baseDirectory));
}

_baseDirectory = baseDirectory;
_shadowCopyDirectoryAndMutex = new Lazy<(string directory, Mutex)>(
() => CreateUniqueDirectoryForProcess(), LazyThreadSafetyMode.ExecutionAndPublication);

Expand All @@ -87,7 +80,7 @@ private void DeleteLeftoverDirectories()
}
catch (DirectoryNotFoundException)
{
return;
return;
}

foreach (var subDirectory in subDirectories)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public RunRequest(Guid requestId, string language, string? workingDirectory, str

internal sealed class CompilerServerHost : ICompilerServerHost
{
public IAnalyzerAssemblyLoader AnalyzerAssemblyLoader { get; } = new ShadowCopyAnalyzerAssemblyLoader(baseDirectory: Path.Combine(Path.GetTempPath(), "VBCSCompiler", "AnalyzerAssemblyLoader"));
public IAnalyzerAssemblyLoader AnalyzerAssemblyLoader { get; } = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(Path.Combine("VBCSCompiler", "AnalyzerAssemblyLoader"));

public static Func<string, MetadataReferenceProperties, PortableExecutableReference> SharedAssemblyReferenceProvider { get; } = (path, properties) => new CachingMetadataReference(path, properties);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis.Diagnostics;
using System.IO;

namespace Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.Analyzers
{
internal static class OmnisharpAnalyzerAssemblyLoaderFactory
{
public static IAnalyzerAssemblyLoader CreateShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null)
{
return new ShadowCopyAnalyzerAssemblyLoader(baseDirectory);
return DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(baseDirectory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ namespace Microsoft.CodeAnalysis.Host
[ExportWorkspaceService(typeof(IAnalyzerAssemblyLoaderProvider))]
internal sealed class DefaultAnalyzerAssemblyLoaderService : IAnalyzerAssemblyLoaderProvider
{
private readonly DefaultAnalyzerAssemblyLoader _loader = new();
private readonly ShadowCopyAnalyzerAssemblyLoader _shadowCopyLoader = new();
private readonly IAnalyzerAssemblyLoader _loader = new DefaultAnalyzerAssemblyLoader();
private readonly IAnalyzerAssemblyLoader _shadowCopyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader();

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal sealed class RemoteAnalyzerAssemblyLoader : AnalyzerAssemblyLoader

public RemoteAnalyzerAssemblyLoader(string baseDirectory)
{
// jason should we load from stream here on Linux or is this VS only?
_baseDirectory = baseDirectory;
}

Expand Down

0 comments on commit 9760b22

Please sign in to comment.