Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose disposing capability on IAssemblyLoaderInternal #74845

Merged
merged 5 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -750,5 +750,6 @@ public void AddDependencyLocation(string fullPath) { }
public bool IsHostAssembly(Assembly assembly) => false;
public Assembly LoadFromPath(string fullPath) => throw new Exception();
public string? GetOriginalDependencyLocation(AssemblyName assembly) => throw new Exception();
public void Dispose() { }
}
}
3 changes: 1 addition & 2 deletions src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compi
var compilerContextCount = compilerContext.Assemblies.Count();

using var tempRoot = new TempRoot();
AnalyzerAssemblyLoader loader = kind switch
using AnalyzerAssemblyLoader loader = kind switch
{
AnalyzerTestKind.LoadDirect => new DefaultAnalyzerAssemblyLoader(compilerContext, AnalyzerLoadOption.LoadFromDisk, externalResolvers.ToImmutableArray()),
AnalyzerTestKind.LoadStream => new DefaultAnalyzerAssemblyLoader(compilerContext, AnalyzerLoadOption.LoadFromStream, externalResolvers.ToImmutableArray()),
Expand All @@ -60,7 +60,6 @@ internal void Exec(ITestOutputHelper testOutputHelper, AssemblyLoadContext compi
}
finally
{
loader.UnloadAll();
testOutputHelper.WriteLine($"Test fixture root: {fixture.TempDirectory}");

foreach (var context in loader.GetDirectoryLoadContextsSnapshot())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis
{
Expand Down Expand Up @@ -57,6 +59,8 @@ internal AnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext, Analyz

public bool IsHostAssembly(Assembly assembly)
{
CheckIfDisposed();

var alc = AssemblyLoadContext.GetLoadContext(assembly);
return alc == _compilerLoadContext || alc == AssemblyLoadContext.Default;
}
Expand All @@ -83,25 +87,37 @@ private partial bool IsMatch(AssemblyName requestedName, AssemblyName candidateN

internal DirectoryLoadContext[] GetDirectoryLoadContextsSnapshot()
{
CheckIfDisposed();

lock (_guard)
{
return _loadContextByDirectory.Values.OrderBy(v => v.Directory).ToArray();
}
}

internal void UnloadAll()
private partial void DisposeWorker()
{
List<DirectoryLoadContext> contexts;
var contexts = ArrayBuilder<DirectoryLoadContext>.GetInstance();
lock (_guard)
{
contexts = _loadContextByDirectory.Values.ToList();
foreach (var (_, context) in _loadContextByDirectory)
contexts.Add(context);

_loadContextByDirectory.Clear();
}

foreach (var context in contexts)
{
context.Unload();
try
{
context.Unload();
}
catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
{
}
}

contexts.Free();
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
}

internal sealed class DirectoryLoadContext : AssemblyLoadContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,15 @@ internal AnalyzerAssemblyLoader(ImmutableArray<IAnalyzerAssemblyResolver> extern
_externalResolvers = externalResolvers;
}

private partial void DisposeWorker()
{
EnsureResolvedUnhooked();
}

public bool IsHostAssembly(Assembly assembly)
{
CheckIfDisposed();

// When an assembly is loaded from the GAC then the load result would be the same if
// this ran on command line compiler. So there is no consistency issue here, this
// is just runtime rules expressing themselves.
Expand Down Expand Up @@ -74,6 +81,8 @@ private partial bool IsMatch(AssemblyName requestedName, AssemblyName candidateN

internal bool EnsureResolvedHooked()
{
CheckIfDisposed();

lock (_guard)
{
if (!_hookedAssemblyResolve)
Expand All @@ -89,6 +98,8 @@ internal bool EnsureResolvedHooked()

internal bool EnsureResolvedUnhooked()
{
// Called from Dispose. We don't want to throw if we're disposed.

lock (_guard)
{
if (_hookedAssemblyResolve)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Microsoft.CodeAnalysis
{
internal interface IAnalyzerAssemblyLoaderInternal : IAnalyzerAssemblyLoader
internal interface IAnalyzerAssemblyLoaderInternal : IAnalyzerAssemblyLoader, IDisposable
{
/// <summary>
/// Is this an <see cref="Assembly"/> that the loader considers to be part of the hosting
Expand Down Expand Up @@ -76,6 +76,11 @@ internal abstract partial class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
/// <see cref="Assembly"/> winning.</remarks>
private readonly ImmutableArray<IAnalyzerAssemblyResolver> _externalResolvers;

/// <summary>
/// Whether or not we're disposed. Once disposed, all functionality on this type should throw.
/// </summary>
private bool _isDisposed;

/// <summary>
/// The implementation needs to load an <see cref="Assembly"/> with the specified <see cref="AssemblyName"/>. The
/// <paramref name="assemblyOriginalPath"/> parameter is the original path. It may be different than
Expand All @@ -93,8 +98,31 @@ internal abstract partial class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
/// </summary>
private partial bool IsMatch(AssemblyName requestedName, AssemblyName candidateName);

private void CheckIfDisposed()
{
#if NET
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
throw new ObjectDisposedException(this.GetType().FullName);
#endif
}

public void Dispose()
{
if (_isDisposed)
return;

_isDisposed = true;
DisposeWorker();
}

private partial void DisposeWorker();

internal bool IsAnalyzerDependencyPath(string fullPath)
{
CheckIfDisposed();

lock (_guard)
{
return _analyzerAssemblyInfoMap.ContainsKey(fullPath);
Expand All @@ -103,6 +131,8 @@ internal bool IsAnalyzerDependencyPath(string fullPath)

public void AddDependencyLocation(string fullPath)
{
CheckIfDisposed();

CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
string simpleName = PathUtilities.GetFileName(fullPath, includeExtension: false);

Expand All @@ -127,6 +157,8 @@ public void AddDependencyLocation(string fullPath)

public Assembly LoadFromPath(string originalAnalyzerPath)
{
CheckIfDisposed();

CompilerPathUtilities.RequireAbsolutePath(originalAnalyzerPath, nameof(originalAnalyzerPath));

(AssemblyName? assemblyName, _) = GetAssemblyInfoForPath(originalAnalyzerPath);
Expand Down Expand Up @@ -158,6 +190,8 @@ public Assembly LoadFromPath(string originalAnalyzerPath)
/// </remarks>
protected (AssemblyName? AssemblyName, string RealAssemblyPath) GetAssemblyInfoForPath(string originalAnalyzerPath)
{
CheckIfDisposed();

lock (_guard)
{
if (!_analyzerAssemblyInfoMap.TryGetValue(originalAnalyzerPath, out var tuple))
Expand Down Expand Up @@ -205,6 +239,8 @@ public Assembly LoadFromPath(string originalAnalyzerPath)
/// </remarks>
internal string? GetRealSatelliteLoadPath(string originalAnalyzerPath, CultureInfo cultureInfo)
{
CheckIfDisposed();

string? realSatelliteAssemblyPath = null;

lock (_guard)
Expand Down Expand Up @@ -250,14 +286,19 @@ public Assembly LoadFromPath(string originalAnalyzerPath)
}
}

public string? GetOriginalDependencyLocation(AssemblyName assemblyName) =>
GetBestPath(assemblyName).BestOriginalPath;
public string? GetOriginalDependencyLocation(AssemblyName assemblyName)
{
CheckIfDisposed();

return GetBestPath(assemblyName).BestOriginalPath;
}
/// <summary>
/// Return the best (original, real) path information for loading an assembly with the specified <see cref="AssemblyName"/>.
/// </summary>
protected (string? BestOriginalPath, string? BestRealPath) GetBestPath(AssemblyName requestedName)
{
CheckIfDisposed();

if (requestedName.Name is null)
{
return (null, null);
Expand Down Expand Up @@ -327,6 +368,8 @@ protected static string GetSatelliteFileName(string assemblyFileName) =>
/// </summary>
internal string GetRealAnalyzerLoadPath(string originalFullPath)
{
CheckIfDisposed();

lock (_guard)
{
if (!_analyzerAssemblyInfoMap.TryGetValue(originalFullPath, out var tuple))
Expand All @@ -340,6 +383,8 @@ internal string GetRealAnalyzerLoadPath(string originalFullPath)

internal (string OriginalAssemblyPath, string RealAssemblyPath)[] GetPathMapSnapshot()
{
CheckIfDisposed();

lock (_guard)
{
return _analyzerAssemblyInfoMap
Expand All @@ -357,6 +402,8 @@ internal string GetRealAnalyzerLoadPath(string originalFullPath)
/// <returns>An <see langword="assembly"/> if one of the resolvers is successful, or <see langword="null"/></returns>
internal Assembly? ResolveAssemblyExternally(AssemblyName assemblyName)
{
CheckIfDisposed();

if (!_externalResolvers.IsDefaultOrEmpty)
{
foreach (var resolver in _externalResolvers)
Expand Down
10 changes: 5 additions & 5 deletions src/Compilers/Core/Portable/InternalUtilities/FatalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@ public static bool ReportAndPropagateUnlessCanceled(Exception exception, Cancell
return ReportAndPropagate(exception, severity);
}

// Since the command line compiler has no way to catch exceptions, report them, and march on, we
// simply don't offer such a mechanism here to avoid accidental swallowing of exceptions.

#if !COMPILERCORE

/// <summary>
/// Report an error.
/// Calls <see cref="s_handler"/> and doesn't pass the exception through (the method returns true).
Expand All @@ -169,6 +164,11 @@ public static bool ReportAndCatch(Exception exception, ErrorSeverity severity =
return true;
}

// Since the command line compiler has no way to catch exceptions, report them, and march on, we
// simply don't offer such a mechanism here to avoid accidental swallowing of exceptions.

#if !COMPILERCORE

[DebuggerHidden]
public static bool ReportWithDumpAndCatch(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,5 +251,6 @@ public void AddDependencyLocation(string fullPath) { }
public bool IsHostAssembly(Assembly assembly) => false;
public Assembly LoadFromPath(string fullPath) => throw new Exception();
public string? GetOriginalDependencyLocation(AssemblyName assembly) => throw new Exception();
public void Dispose() { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal sealed class VSCodeAnalyzerLoaderProvider(
private readonly ExtensionAssemblyManager _extensionAssemblyManager = extensionAssemblyManager;
private readonly ILoggerFactory _loggerFactory = loggerFactory;

protected override IAnalyzerAssemblyLoader CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
protected override IAnalyzerAssemblyLoaderInternal CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
{
var baseLoader = base.CreateShadowCopyLoader(externalResolvers);
return new VSCodeExtensionAssemblyAnalyzerLoader(baseLoader, _extensionAssemblyManager, _loggerFactory.CreateLogger<VSCodeExtensionAssemblyAnalyzerLoader>());
Expand All @@ -33,12 +33,19 @@ protected override IAnalyzerAssemblyLoader CreateShadowCopyLoader(ImmutableArray
/// <summary>
/// Analyzer loader that will re-use already loaded assemblies from the extension load context.
/// </summary>
private class VSCodeExtensionAssemblyAnalyzerLoader(IAnalyzerAssemblyLoader defaultLoader, ExtensionAssemblyManager extensionAssemblyManager, ILogger logger) : IAnalyzerAssemblyLoader
private sealed class VSCodeExtensionAssemblyAnalyzerLoader(
IAnalyzerAssemblyLoaderInternal defaultLoader,
ExtensionAssemblyManager extensionAssemblyManager,
ILogger logger) : IAnalyzerAssemblyLoaderInternal
{
public void AddDependencyLocation(string fullPath)
{
defaultLoader.AddDependencyLocation(fullPath);
}
=> defaultLoader.AddDependencyLocation(fullPath);

public string? GetOriginalDependencyLocation(AssemblyName assembly)
=> defaultLoader.GetOriginalDependencyLocation(assembly);

public bool IsHostAssembly(Assembly assembly)
=> defaultLoader.IsHostAssembly(assembly);

public Assembly LoadFromPath(string fullPath)
{
Expand All @@ -51,5 +58,8 @@ public Assembly LoadFromPath(string fullPath)

return defaultLoader.LoadFromPath(fullPath);
}

public void Dispose()
=> defaultLoader.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@ namespace Microsoft.CodeAnalysis.Host;
/// </summary>
internal abstract class AbstractAnalyzerAssemblyLoaderProvider : IAnalyzerAssemblyLoaderProvider
{
private readonly Lazy<IAnalyzerAssemblyLoader> _shadowCopyLoader;
private readonly Lazy<IAnalyzerAssemblyLoaderInternal> _shadowCopyLoader;

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

public IAnalyzerAssemblyLoader GetShadowCopyLoader()
public IAnalyzerAssemblyLoaderInternal GetShadowCopyLoader()
=> _shadowCopyLoader.Value;

protected virtual IAnalyzerAssemblyLoader CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
{
return DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(
Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader"),
protected virtual IAnalyzerAssemblyLoaderInternal CreateShadowCopyLoader(ImmutableArray<IAnalyzerAssemblyResolver> externalResolvers)
=> DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(
GetDefaultShadowCopyPath(),
externalResolvers: externalResolvers);
}

public static string GetDefaultShadowCopyPath()
=> Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader");
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ private sealed class DefaultAnalyzerAssemblyLoaderProvider(string workspaceKind,
/// locking it. The exception is fine, since the cleanup is just hygienic and isn't intended to be needed for
/// correctness. But it is annoying and does cause noise in our perf test harness.
/// </summary>
private readonly IAnalyzerAssemblyLoader _shadowCopyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(
private readonly IAnalyzerAssemblyLoaderInternal _shadowCopyLoader = DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader(
Path.Combine(Path.GetTempPath(), "CodeAnalysis", "WorkspacesAnalyzerShadowCopies", workspaceKind),
externalResolvers: externalResolvers);

public IAnalyzerAssemblyLoader GetShadowCopyLoader()
public IAnalyzerAssemblyLoaderInternal GetShadowCopyLoader()
=> _shadowCopyLoader;
}
}
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 GetShadowCopyLoader();
IAnalyzerAssemblyLoaderInternal GetShadowCopyLoader();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ internal sealed class RemoteAnalyzerAssemblyLoaderService(
: IAnalyzerAssemblyLoaderProvider
{
private readonly ShadowCopyAnalyzerAssemblyLoader _shadowCopyLoader =
new(Path.Combine(Path.GetTempPath(), "VS", "AnalyzerAssemblyLoader"), externalResolvers.ToImmutableArray());
new(AbstractAnalyzerAssemblyLoaderProvider.GetDefaultShadowCopyPath(), externalResolvers.ToImmutableArray());

public IAnalyzerAssemblyLoader GetShadowCopyLoader()
public IAnalyzerAssemblyLoaderInternal GetShadowCopyLoader()
=> _shadowCopyLoader;
}
Loading