Add AssemblyLoadContext for loading extensions (#3649)
authorMike McLaughlin <mikem@microsoft.com>
Wed, 15 Mar 2023 22:46:08 +0000 (15:46 -0700)
committerGitHub <noreply@github.com>
Wed, 15 Mar 2023 22:46:08 +0000 (15:46 -0700)
* Add AssemblyLoadContext for loading extensions

Add and use ServiceManager.NotifyExtensionLoadFailure event.

Replace Provider scope with ProviderExport attribute

Removing the DAC or DBI check in GetLocalPath() keeps invalid DACs or DBIs
from being loaded if in the current directory. The download path will find
a DAC in the same directory as the core dump when under dotnet-dump.

Update extensibility doc

* Update to 7.0.3 and 6.0.14

* Change the context service ordering to prevent creating runtimes if not needed

* Fix analyzer issues

* Fix overflow in WebApp tests

* Code review feedback

* Fix desktop SOS tests

16 files changed:
documentation/design-docs/dotnet-dump-extensibility.md
eng/Versions.props
src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices.Implementation/ContextService.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/ImageMappingMemoryService.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/Microsoft.Diagnostics.DebugServices.Implementation.csproj
src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs
src/Microsoft.Diagnostics.DebugServices/CommandServiceExtensions.cs
src/Microsoft.Diagnostics.DebugServices/Microsoft.Diagnostics.DebugServices.csproj
src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs
src/SOS/SOS.Extensions/AssemblyResolver.cs [deleted file]
src/SOS/SOS.Extensions/HostServices.cs
src/Tools/dotnet-dump/Analyzer.cs

index 3648bea9889644be84ae69870e89b10edf4f75eb..8afd658bf8c32e5c7043d04dfc9e690609ab1de0 100644 (file)
@@ -111,7 +111,7 @@ The threading model is single-threaded mainly because native debuggers like dbge
 
 The host is the debugger or program the command and the infrastructure runs on. The goal is to allow the same code for a command to run under different programs like the dotnet-dump REPL, lldb and Windows debuggers. Under Visual Studio the host will be a VS extension package.
 
-When the host starts, the service manager loads command and service extension assemblies from the DOTNET_DIAGNOSTIC_EXTENSIONS environment variable (assembly paths separated by ';') or from the $HOME/.dotnet/extensions directory on Linux or MacOS or %USERPROFILE%\.dotnet\extensions directory on Windows.
+When the host starts, the service manager loads command and service extension assemblies from the DOTNET_DIAGNOSTIC_EXTENSIONS environment variable (assembly paths separated by ';' on Windows or ":" on Linux/MacOS) or under the subdirectory "extensions" in the same directory as the infrastructure assemblies (Microsoft.Diagnostics.DebugServices.Implementation).
 
 #### IHost
 
@@ -149,11 +149,11 @@ Services can be registered to contain common code for commands like [ClrMDHelper
 
 The [ServiceExport](../../src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs) attribute is used to mark classes, class constructors and factory methods to be registered as services. The ServiceScope defines where in the service hierarchy (global, context, target, runtime, thread, or module) this instance is available to commands and other services.
 
-The [ServiceImport](../../src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs) attribute is used to mark public or interal fields, properties and methods in commands and other services to receive a service instance.
+The [ProviderExport](../../src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs) attribute is used to mark classes, class constructors and factory methods to be registered as "provider" which are extensions to a service. The IRuntimeService implementation uses this feature to enumerate all the IRuntimeProvider instances registered in the system.
 
-The internal [ServiceManager](../../src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs) loads extension assemblies, provides the dependency injection using reflection (via the above attributes) and manages the various service factories. It creates the [IServiceContainer](../../src/Microsoft.Diagnostics.DebugServices/IServiceContainer.cs) instances for the extension points globally, in targets, modules, threads and runtimes (i.e. the IRuntime.ServiceProvider property). The public [IServiceManager](../../src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs) interface exposes the public methods of the manager.
+The [ServiceImport](../../src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs) attribute is used to mark public or interal fields, properties and methods in commands and other services to receive a service instance.
 
-The IServiceProvider/IServiceContainer implementation allows multiple instances of the same service type to be registered. They are queried by getting the IEnumerable of the service type (i.e. calling `IServiceProvider.GetService(typeof(IEnumerable<service type>)`). If the non-enumerable service type is queried and there are multiple instances, an exception is thrown. The IRuntimeService implementation uses this feature to enumerate all the IRuntimeProvider instances registered in the system.
+The internal [ServiceManager](../../src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs) loads extension assemblies, provides the dependency injection using reflection (via the above attributes) and manages the various service factories. It creates the [ServiceContainerFactory](../../src/Microsoft.Diagnostics.DebugServices/ServiceContainerFactory.cs) instances for the extension points globally, in targets, modules, threads and runtimes (i.e. the IRuntime.ServiceProvider property). From the ServiceContainerFactory [ServiceContainer](../../src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs) instances are built. The public [IServiceManager](../../src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs) interface exposes the public methods of the manager.
 
 ### IDumpTargetFactory
 
@@ -239,7 +239,7 @@ This interface provides services to the native SOS/plugins code. It is a private
 
 This native interface is what the SOS.Extensions host uses to implement the above services. This is another private interface between SOS.Extensions and the native lldb plugin or Windows SOS native code.
 
-[debuggerservice.h](../../src/SOS/inc/debuggerservice.h) for details.
+[debuggerservice.h](../../src/SOS/inc/debuggerservices.h) for details.
 
 ## Projects and Assemblies
 
index a557ae79222cd47719ddccb5c1e8f3452e8d1dde..3375ffb6c0879312b019c8a195eb48217d8a8b79 100644 (file)
   </PropertyGroup>
   <PropertyGroup>
     <!-- Runtime versions to test -->
-    <MicrosoftNETCoreApp60Version>6.0.12</MicrosoftNETCoreApp60Version>
+    <MicrosoftNETCoreApp60Version>6.0.14</MicrosoftNETCoreApp60Version>
     <MicrosoftAspNetCoreApp60Version>$(MicrosoftNETCoreApp60Version)</MicrosoftAspNetCoreApp60Version>
-    <MicrosoftNETCoreApp70Version>7.0.2</MicrosoftNETCoreApp70Version>
+    <MicrosoftNETCoreApp70Version>7.0.3</MicrosoftNETCoreApp70Version>
     <MicrosoftAspNetCoreApp70Version>$(MicrosoftNETCoreApp70Version)</MicrosoftAspNetCoreApp70Version>
     <!-- The SDK runtime version used to build single-file apps (currently hardcoded) -->
     <SingleFileRuntime60Version>$(MicrosoftNETCoreApp60Version)</SingleFileRuntime60Version>
-    <SingleFileRuntime70Version>$(MicrosoftNETCoreApp70Version)</SingleFileRuntime70Version>
+    <SingleFileRuntime70Version>7.0.2</SingleFileRuntime70Version>
     <SingleFileRuntimeLatestVersion>8.0.0-preview.2.23127.4</SingleFileRuntimeLatestVersion>
   </PropertyGroup>
   <PropertyGroup>
@@ -59,6 +59,7 @@
     <SystemCommandLineRenderingVersion>2.0.0-beta1.20074.1</SystemCommandLineRenderingVersion>
     <SystemComponentModelAnnotationsVersion>5.0.0</SystemComponentModelAnnotationsVersion>
     <SystemMemoryVersion>4.5.4</SystemMemoryVersion>
+    <SystemRuntimeLoaderVersion>4.3.0</SystemRuntimeLoaderVersion>
     <SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
     <SystemTextJsonVersion>4.7.1</SystemTextJsonVersion>
     <XUnitAbstractionsVersion>2.0.3</XUnitAbstractionsVersion>
diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/AssemblyResolver.cs
new file mode 100644 (file)
index 0000000..95eb369
--- /dev/null
@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+
+namespace Microsoft.Diagnostics.DebugServices.Implementation
+{
+    /// <summary>
+    /// Used to enable app-local assembly unification.
+    /// </summary>
+    public static class AssemblyResolver
+    {
+        private static readonly string _defaultAssembliesPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+        private static bool _initialized;
+
+        /// <summary>
+        /// Call to enable the assembly resolver for the current AppDomain.
+        /// </summary>
+        public static void Enable()
+        {
+            if (!_initialized)
+            {
+                _initialized = true;
+                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
+            }
+        }
+
+        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
+        {
+            // apply any existing policy
+            AssemblyName referenceName = new(AppDomain.CurrentDomain.ApplyPolicy(args.Name));
+            string fileName = referenceName.Name + ".dll";
+            string assemblyPath;
+            string probingPath;
+            Assembly assembly;
+
+            // Look next to requesting assembly
+            assemblyPath = args.RequestingAssembly?.Location;
+            if (!string.IsNullOrEmpty(assemblyPath))
+            {
+                probingPath = Path.Combine(Path.GetDirectoryName(assemblyPath), fileName);
+                Debug.WriteLine($"Considering {probingPath} based on RequestingAssembly");
+                if (Probe(probingPath, referenceName.Version, out assembly))
+                {
+                    Debug.WriteLine($"Matched {probingPath} based on RequestingAssembly");
+                    return assembly;
+                }
+            }
+
+            // Look next to the executing assembly
+            probingPath = Path.Combine(_defaultAssembliesPath, fileName);
+            Debug.WriteLine($"Considering {probingPath} based on ExecutingAssembly");
+            if (Probe(probingPath, referenceName.Version, out assembly))
+            {
+                Debug.WriteLine($"Matched {probingPath} based on ExecutingAssembly");
+                return assembly;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Considers a path to load for satisfying an assembly ref and loads it
+        /// if the file exists and version is sufficient.
+        /// </summary>
+        /// <param name="filePath">Path to consider for load</param>
+        /// <param name="minimumVersion">Minimum version to consider</param>
+        /// <param name="assembly">loaded assembly</param>
+        /// <returns>true if assembly was loaded</returns>
+        private static bool Probe(string filePath, Version minimumVersion, out Assembly assembly)
+        {
+            if (File.Exists(filePath))
+            {
+                AssemblyName name = AssemblyName.GetAssemblyName(filePath);
+                if (name.Version >= minimumVersion)
+                {
+                    assembly = Assembly.LoadFile(filePath);
+                    return true;
+                }
+            }
+            assembly = null;
+            return false;
+        }
+    }
+}
index 9857aea68404c1cd31855033de877f95ce3c9676..e4df926135543892725c9498b67068ea13db20b5 100644 (file)
@@ -266,6 +266,17 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
                 {
                     return _contextService.GetCurrentTarget();
                 }
+                // Check the current target (if exists) for the service.
+                ITarget currentTarget = _contextService.GetCurrentTarget();
+                if (currentTarget is not null)
+                {
+                    // This will chain to the global services if not found in the current target
+                    object service = currentTarget.Services.GetService(type);
+                    if (service is not null)
+                    {
+                        return service;
+                    }
+                }
                 // Check the current runtime (if exists) for the service.
                 IRuntime currentRuntime = _contextService.GetCurrentRuntime();
                 if (currentRuntime is not null)
@@ -288,17 +299,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
                         return service;
                     }
                 }
-                // Check the current target (if exists) for the service.
-                ITarget currentTarget = _contextService.GetCurrentTarget();
-                if (currentTarget is not null)
-                {
-                    // This will chain to the global services if not found in the current target
-                    object service = currentTarget.Services.GetService(type);
-                    if (service is not null)
-                    {
-                        return service;
-                    }
-                }
                 // Check with the global host services.
                 return _contextService._host.Services.GetService(type);
             }
index 246343e3a462fd65d8ff40047f6de3a12c043629..35fb9c9afc53f20119e328a098612d232fdc1888 100644 (file)
@@ -300,7 +300,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
                                         if ((offset + sizeof(ulong)) <= data.Length)
                                         {
                                             ulong value = BitConverter.ToUInt64(data, offset);
-                                            value += baseDelta;
+                                            unchecked { value += baseDelta; }
                                             byte[] source = BitConverter.GetBytes(value);
                                             Array.Copy(source, 0, data, offset, source.Length);
                                         }
index d36528d89ab3d2ab346328ff3f115d8eadf36b5f..06c99c30c29748ea7f8946d4cc044f85520af3d6 100644 (file)
@@ -19,6 +19,7 @@
     <PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
     <PackageReference Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
     <PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" />
+    <PackageReference Include="System.Runtime.Loader" Version="$(SystemRuntimeLoaderVersion)" />
   </ItemGroup>
   
   <ItemGroup>
index 8b9e352e4ee5b7cc6beee163094a3df440ebfe15..40b237f88410628b5cb3ef00493521c2c3dca294 100644 (file)
@@ -177,10 +177,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
 
         private string GetLocalPath(string fileName)
         {
-            if (File.Exists(fileName))
-            {
-                return fileName;
-            }
             string localFilePath;
             if (!string.IsNullOrEmpty(RuntimeModuleDirectory))
             {
index 24b0222f8d12d3713a791f5b2aa440208cc785ad..a755a13d6dc16441d72bb26c711ae5e4c555c7c7 100644 (file)
@@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
     /// <summary>
     /// ClrMD runtime provider implementation
     /// </summary>
-    [ServiceExport(Type = typeof(IRuntimeProvider), Scope = ServiceScope.Provider)]
+    [ProviderExport(Type = typeof(IRuntimeProvider))]
     public class RuntimeProvider : IRuntimeProvider
     {
         private readonly IServiceProvider _services;
index 714f3076da29fec16292b1f5cf74ec3333e942c5..090270e28887e30b69533602948c15f386387380 100644 (file)
@@ -6,6 +6,8 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
 
 namespace Microsoft.Diagnostics.DebugServices.Implementation
 {
@@ -18,13 +20,29 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
     {
         private readonly Dictionary<Type, ServiceFactory>[] _factories;
         private readonly Dictionary<Type, List<ServiceFactory>> _providerFactories;
-        private readonly ServiceEvent<Assembly> _notifyExtensionLoad;
+        private readonly List<object> _extensions;
         private bool _finalized;
 
         /// <summary>
         /// This event fires when an extension assembly is loaded
         /// </summary>
-        public IServiceEvent<Assembly> NotifyExtensionLoad => _notifyExtensionLoad;
+        public IServiceEvent<Assembly> NotifyExtensionLoad { get; }
+
+        /// <summary>
+        /// This event fires when an extension assembly fails
+        /// </summary>
+        public IServiceEvent<Exception> NotifyExtensionLoadFailure { get; }
+
+        /// <summary>
+        /// Enable the assembly resolver on desktop Framework
+        /// </summary>
+        static ServiceManager()
+        {
+            if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
+            {
+                AssemblyResolver.Enable();
+            }
+        }
 
         /// <summary>
         /// Create a service manager instance
@@ -33,7 +51,9 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         {
             _factories = new Dictionary<Type, ServiceFactory>[(int)ServiceScope.Max];
             _providerFactories = new Dictionary<Type, List<ServiceFactory>>();
-            _notifyExtensionLoad = new ServiceEvent<Assembly>();
+            _extensions = new List<object>();
+            NotifyExtensionLoad = new ServiceEvent<Assembly>();
+            NotifyExtensionLoadFailure = new ServiceEvent<Exception>();
             for (int i = 0; i < (int)ServiceScope.Max; i++)
             {
                 _factories[i] = new Dictionary<Type, ServiceFactory>();
@@ -44,7 +64,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         /// Creates a new service container factory with all the registered factories for the given scope.
         /// </summary>
         /// <param name="scope">global, per-target, per-runtime, etc. service type</param>
-        /// <param name="parent">parent service provider to chain</param>
+        /// <param name="parent">parent service services to chain</param>
         /// <returns></returns>
         public ServiceContainerFactory CreateServiceContainerFactory(ServiceScope scope, IServiceProvider parent)
         {
@@ -52,22 +72,20 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             {
                 throw new InvalidOperationException();
             }
-
             return new ServiceContainerFactory(parent, _factories[(int)scope]);
         }
 
         /// <summary>
-        /// Get the provider factories for a type or interface.
+        /// Get the services factories for a type or interface.
         /// </summary>
         /// <param name="providerType">type or interface</param>
-        /// <returns>the provider factories for the type</returns>
+        /// <returns>the services factories for the type</returns>
         public IEnumerable<ServiceFactory> EnumerateProviderFactories(Type providerType)
         {
             if (!_finalized)
             {
                 throw new InvalidOperationException();
             }
-
             if (_providerFactories.TryGetValue(providerType, out List<ServiceFactory> factories))
             {
                 return factories;
@@ -79,6 +97,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         /// Finds all the ServiceExport attributes in the assembly and registers.
         /// </summary>
         /// <param name="assembly">service implementation assembly</param>
+        /// <exception cref="FileNotFoundException">assembly or reference not found</exception>
+        /// <exception cref="NotSupportedException">not supported</exception>
         public void RegisterExportedServices(Assembly assembly)
         {
             foreach (Type serviceType in assembly.GetExportedTypes())
@@ -100,7 +120,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             {
                 throw new InvalidOperationException();
             }
-
             for (Type currentType = serviceType; currentType is not null; currentType = currentType.BaseType)
             {
                 if (currentType == typeof(object) || currentType == typeof(ValueType))
@@ -110,23 +129,63 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
                 ServiceExportAttribute serviceAttribute = currentType.GetCustomAttribute<ServiceExportAttribute>(inherit: false);
                 if (serviceAttribute is not null)
                 {
-                    ServiceFactory factory = (provider) => Utilities.CreateInstance(serviceType, provider);
+                    ServiceFactory factory = (services) => Utilities.CreateInstance(serviceType, services);
                     AddServiceFactory(serviceAttribute.Scope, serviceAttribute.Type ?? serviceType, factory);
                 }
+                ProviderExportAttribute providerAttribute = currentType.GetCustomAttribute<ProviderExportAttribute>(inherit: false);
+                if (providerAttribute is not null)
+                {
+                    ServiceFactory factory = (services) => Utilities.CreateInstance(serviceType, services);
+                    AddProviderFactory(providerAttribute.Type ?? serviceType, factory);
+                }
                 // The method or constructor must be static and public
                 foreach (MethodInfo methodInfo in currentType.GetMethods(BindingFlags.Static | BindingFlags.Public))
                 {
                     serviceAttribute = methodInfo.GetCustomAttribute<ServiceExportAttribute>(inherit: false);
                     if (serviceAttribute is not null)
                     {
-                        AddServiceFactory(serviceAttribute.Scope, serviceAttribute.Type ?? methodInfo.ReturnType, (provider) => Utilities.CreateInstance(methodInfo, provider));
+                        ServiceFactory factory = (services) => Utilities.CreateInstance(methodInfo, services);
+                        AddServiceFactory(serviceAttribute.Scope, serviceAttribute.Type ?? methodInfo.ReturnType, factory);
+                    }
+                    providerAttribute = currentType.GetCustomAttribute<ProviderExportAttribute>(inherit: false);
+                    if (providerAttribute is not null)
+                    {
+                        ServiceFactory factory = (services) => Utilities.CreateInstance(methodInfo, services);
+                        AddProviderFactory(providerAttribute.Type ?? methodInfo.ReturnType, factory);
                     }
                 }
             }
         }
 
         /// <summary>
-        /// Add service containerFactory for the specific scope.
+        /// Register the exported services in the assembly and notify the assembly has loaded.
+        /// </summary>
+        /// <param name="assembly">extension assembly</param>
+        public void RegisterAssembly(Assembly assembly)
+        {
+            if (_finalized)
+            {
+                throw new InvalidOperationException();
+            }
+            try
+            {
+                RegisterExportedServices(assembly);
+                NotifyExtensionLoad.Fire(assembly);
+            }
+            catch (Exception ex) when
+                (ex is DiagnosticsException
+                 or ArgumentException
+                 or NotSupportedException
+                 or FileLoadException
+                 or FileNotFoundException)
+            {
+                Trace.TraceError(ex.ToString());
+                NotifyExtensionLoadFailure.Fire(new DiagnosticsException($"Extension load failure - {ex.Message} {assembly.Location}", ex));
+            }
+        }
+
+        /// <summary>
+        /// Add service factory for the specific scope.
         /// </summary>
         /// <typeparam name="T">service type</typeparam>
         /// <param name="scope">global, per-target, per-runtime, etc. service type</param>
@@ -134,7 +193,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         public void AddServiceFactory<T>(ServiceScope scope, ServiceFactory factory) => AddServiceFactory(scope, typeof(T), factory);
 
         /// <summary>
-        /// Add service containerFactory for the specific scope.
+        /// Add service factory for the specific scope.
         /// </summary>
         /// <param name="scope">global, per-target, per-runtime, etc. service type</param>
         /// <param name="serviceType">service type or interface</param>
@@ -145,26 +204,40 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             {
                 throw new ArgumentNullException(nameof(factory));
             }
-
             if (_finalized)
             {
                 throw new InvalidOperationException();
             }
+            _factories[(int)scope].Add(serviceType, factory);
+        }
 
-            if (scope == ServiceScope.Provider)
+        /// <summary>
+        /// Add provider factory.
+        /// </summary>
+        /// <param name="providerType">service type or interface</param>
+        /// <param name="factory">function to create provider instance</param>
+        public void AddProviderFactory(Type providerType, ServiceFactory factory)
+        {
+            if (factory is null)
             {
-                if (!_providerFactories.TryGetValue(serviceType, out List<ServiceFactory> factories))
-                {
-                    _providerFactories.Add(serviceType, factories = new List<ServiceFactory>());
-                }
-                factories.Add(factory);
+                throw new ArgumentNullException(nameof(factory));
             }
-            else
+            if (_finalized)
             {
-                _factories[(int)scope].Add(serviceType, factory);
+                throw new InvalidOperationException();
+            }
+            if (!_providerFactories.TryGetValue(providerType, out List<ServiceFactory> factories))
+            {
+                _providerFactories.Add(providerType, factories = new List<ServiceFactory>());
             }
+            factories.Add(factory);
         }
 
+        /// <summary>
+        /// Finalizes the service manager. Loading extensions or adding service factories are not allowed after this call.
+        /// </summary>
+        public void FinalizeServices() => _finalized = true;
+
         /// <summary>
         /// Load any extra extensions in the search path
         /// </summary>
@@ -174,7 +247,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             {
                 throw new InvalidOperationException();
             }
-
             List<string> extensionPaths = new();
             string diagnosticExtensions = Environment.GetEnvironmentVariable("DOTNET_DIAGNOSTIC_EXTENSIONS");
             if (!string.IsNullOrEmpty(diagnosticExtensions))
@@ -193,7 +265,12 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
                         string[] extensionFiles = Directory.GetFiles(searchPath, "*.dll");
                         extensionPaths.AddRange(extensionFiles);
                     }
-                    catch (Exception ex) when (ex is IOException or ArgumentException or UnauthorizedAccessException or System.Security.SecurityException)
+                    catch (Exception ex) when
+                        (ex is IOException
+                         or ArgumentException
+                         or BadImageFormatException
+                         or UnauthorizedAccessException
+                         or System.Security.SecurityException)
                     {
                         Trace.TraceError(ex.ToString());
                     }
@@ -215,15 +292,28 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
             {
                 throw new InvalidOperationException();
             }
-
             Assembly assembly = null;
             try
             {
-                assembly = Assembly.LoadFrom(extensionPath);
+                // Assembly load contexts are not supported by the desktop framework
+                if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
+                {
+                    assembly = Assembly.LoadFile(extensionPath);
+                }
+                else
+                {
+                    assembly = UseAssemblyLoadContext(extensionPath);
+                }
             }
-            catch (Exception ex) when (ex is IOException or ArgumentException or BadImageFormatException or System.Security.SecurityException)
+            catch (Exception ex) when
+                (ex is IOException
+                 or ArgumentException
+                 or InvalidOperationException
+                 or BadImageFormatException
+                 or System.Security.SecurityException)
             {
                 Trace.TraceError(ex.ToString());
+                NotifyExtensionLoadFailure.Fire(ex);
             }
             if (assembly is not null)
             {
@@ -232,30 +322,79 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation
         }
 
         /// <summary>
-        /// Register the exported services in the assembly and notify the assembly has loaded.
+        /// Load the extension using an assembly load context. This needs to be in
+        /// a separate method so ExtensionLoadContext class doesn't get referenced
+        /// when running on desktop Framework.
         /// </summary>
-        /// <param name="assembly">extension assembly</param>
-        public void RegisterAssembly(Assembly assembly)
+        /// <param name="extensionPath">extension assembly path</param>
+        /// <returns>assembly</returns>
+        private Assembly UseAssemblyLoadContext(string extensionPath)
         {
-            if (_finalized)
+            ExtensionLoadContext extension = new(extensionPath);
+            Assembly assembly = extension.LoadFromAssemblyPath(extensionPath);
+            if (assembly is not null)
             {
-                throw new InvalidOperationException();
+                // This list is just to keep the load context alive
+                _extensions.Add(extension);
             }
+            return assembly;
+        }
 
-            try
+        private sealed class ExtensionLoadContext : AssemblyLoadContext
+        {
+            private static readonly HashSet<string> s_defaultAssemblies = new() {
+                "Microsoft.Diagnostics.DebugServices",
+                "Microsoft.Diagnostics.DebugServices.Implementation",
+                "Microsoft.Diagnostics.ExtensionCommands",
+                "Microsoft.Diagnostics.NETCore.Client",
+                "Microsoft.Diagnostics.Repl",
+                "Microsoft.Diagnostics.Runtime",
+                "Microsoft.FileFormats",
+                "Microsoft.SymbolStore",
+                "SOS.Extensions",
+                "SOS.Hosting",
+                "SOS.InstallHelper"
+            };
+
+            private static readonly string _defaultAssembliesPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+            private readonly string _extensionPath;
+            private Dictionary<string, string> _extensionPaths;
+
+            public ExtensionLoadContext(string extensionPath)
             {
-                RegisterExportedServices(assembly);
-                _notifyExtensionLoad.Fire(assembly);
+                _extensionPath = extensionPath;
             }
-            catch (Exception ex) when (ex is DiagnosticsException or NotSupportedException or FileNotFoundException)
+
+            protected override Assembly Load(AssemblyName assemblyName)
             {
-                Trace.TraceError(ex.ToString());
+                lock (this)
+                {
+                    if (_extensionPaths == null)
+                    {
+                        string[] extensionFiles = Directory.GetFiles(Path.GetDirectoryName(_extensionPath), "*.dll");
+                        _extensionPaths = new Dictionary<string, string>();
+                        foreach (string file in extensionFiles)
+                        {
+                            _extensionPaths.Add(Path.GetFileNameWithoutExtension(file), file);
+                        }
+                    }
+                }
+                if (s_defaultAssemblies.Contains(assemblyName.Name))
+                {
+                    Assembly assembly = Default.LoadFromAssemblyPath(Path.Combine(_defaultAssembliesPath, assemblyName.Name) + ".dll");
+                    if (assemblyName.Version.Major != assembly.GetName().Version.Major)
+                    {
+                        throw new InvalidOperationException($"Extension assembly reference version not supported for {assemblyName.Name} {assemblyName.Version}");
+                    }
+                    return assembly;
+                }
+                else if (_extensionPaths.TryGetValue(assemblyName.Name, out string path))
+                {
+                    return LoadFromAssemblyPath(path);
+                }
+                return null;
             }
         }
-
-        /// <summary>
-        /// Finalizes the service manager. Loading extensions or adding service factories are not allowed after this call.
-        /// </summary>
-        public void FinalizeServices() => _finalized = true;
     }
 }
index 21a1318cc73a813426a1a92fa0a1e580f3a60722..b88f43a3f2bc0f45fa9eccbda27480074d301bd3 100644 (file)
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Reflection;
 
@@ -15,6 +16,8 @@ namespace Microsoft.Diagnostics.DebugServices
         /// </summary>
         /// <param name="commandService">command service instance</param>
         /// <param name="assemblies">list of assemblies to search</param>
+        /// <exception cref="FileNotFoundException">assembly or reference not found</exception>
+        /// <exception cref="NotSupportedException">not supported</exception>
         public static void AddCommands(this ICommandService commandService, IEnumerable<Assembly> assemblies)
         {
             commandService.AddCommands(assemblies.SelectMany((assembly) => assembly.GetExportedTypes()));
@@ -25,6 +28,8 @@ namespace Microsoft.Diagnostics.DebugServices
         /// </summary>
         /// <param name="commandService">command service instance</param>
         /// <param name="assembly">assembly to search for commands</param>
+        /// <exception cref="FileNotFoundException">assembly or reference not found</exception>
+        /// <exception cref="NotSupportedException">not supported</exception>
         public static void AddCommands(this ICommandService commandService, Assembly assembly)
         {
             commandService.AddCommands(assembly.GetExportedTypes());
index a441d0e6bd2a3de0056cb358e0df4be2bfb19614..33f0fe3e9028036dec7eb314407f7588d39b1b4d 100644 (file)
@@ -4,6 +4,7 @@
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <NoWarn>;1591;1701</NoWarn>
     <Description>Diagnostics debug services</Description>
+    <VersionPrefix>7.0.0</VersionPrefix>
     <IsPackable>true</IsPackable>
     <PackageTags>Diagnostic</PackageTags>
     <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
diff --git a/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs b/src/Microsoft.Diagnostics.DebugServices/ProviderExportAttribute.cs
new file mode 100644 (file)
index 0000000..8a70aff
--- /dev/null
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.Diagnostics.DebugServices
+{
+    /// <summary>
+    /// Marks classes or methods (provider factories) as providers (extensions to services).
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+    public class ProviderExportAttribute : Attribute
+    {
+        /// <summary>
+        /// The interface or type to register the provider. If null, the provider type registered will be
+        /// he class itself or the return type of the method.
+        /// </summary>
+        public Type Type { get; set; }
+
+        /// <summary>
+        /// Default constructor.
+        /// </summary>
+        public ProviderExportAttribute()
+        {
+        }
+    }
+}
index 1e22738f62c74bff0a85c4448b85e065d3c19907..2258e8caccd4f7b38532b2a92fe393b09a2f4049 100644 (file)
@@ -9,7 +9,6 @@ namespace Microsoft.Diagnostics.DebugServices
     {
         Global,
         Context,
-        Provider,
         Target,
         Module,
         Thread,
@@ -24,8 +23,8 @@ namespace Microsoft.Diagnostics.DebugServices
     public class ServiceExportAttribute : Attribute
     {
         /// <summary>
-        /// The interface or type to register the service. If null, the service type registered will be any
-        /// interfaces on the the class, the class itself if no interfaces or the return type of the method.
+        /// The interface or type to register the service. If null, the service type registered will be
+        /// the class itself or the return type of the method.
         /// </summary>
         public Type Type { get; set; }
 
diff --git a/src/SOS/SOS.Extensions/AssemblyResolver.cs b/src/SOS/SOS.Extensions/AssemblyResolver.cs
deleted file mode 100644 (file)
index 554ee2e..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Reflection;
-
-namespace SOS.Extensions
-{
-    /// <summary>
-    /// Used to enable app-local assembly unification.
-    /// </summary>
-    public static class AssemblyResolver
-    {
-        private static bool s_initialized;
-
-        /// <summary>
-        /// Call to enable the assembly resolver for the current AppDomain.
-        /// </summary>
-        public static void Enable()
-        {
-            if (!s_initialized)
-            {
-                s_initialized = true;
-                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
-            }
-        }
-
-        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
-        {
-            // apply any existing policy
-            AssemblyName referenceName = new(AppDomain.CurrentDomain.ApplyPolicy(args.Name));
-            string fileName = referenceName.Name + ".dll";
-            string assemblyPath;
-            string probingPath;
-            Assembly assembly;
-
-            // Look next to requesting assembly
-            assemblyPath = args.RequestingAssembly?.Location;
-            if (!string.IsNullOrEmpty(assemblyPath))
-            {
-                probingPath = Path.Combine(Path.GetDirectoryName(assemblyPath), fileName);
-                Debug.WriteLine($"Considering {probingPath} based on RequestingAssembly");
-                if (Probe(probingPath, referenceName.Version, out assembly))
-                {
-                    Debug.WriteLine($"Matched {probingPath} based on RequestingAssembly");
-                    return assembly;
-                }
-            }
-
-            // Look next to the executing assembly
-            assemblyPath = Assembly.GetExecutingAssembly().Location;
-            if (!string.IsNullOrEmpty(assemblyPath))
-            {
-                probingPath = Path.Combine(Path.GetDirectoryName(assemblyPath), fileName);
-                Debug.WriteLine($"Considering {probingPath} based on ExecutingAssembly");
-                if (Probe(probingPath, referenceName.Version, out assembly))
-                {
-                    Debug.WriteLine($"Matched {probingPath} based on ExecutingAssembly");
-                    return assembly;
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Considers a path to load for satisfying an assembly ref and loads it
-        /// if the file exists and version is sufficient.
-        /// </summary>
-        /// <param name="filePath">Path to consider for load</param>
-        /// <param name="minimumVersion">Minimum version to consider</param>
-        /// <param name="assembly">loaded assembly</param>
-        /// <returns>true if assembly was loaded</returns>
-        private static bool Probe(string filePath, Version minimumVersion, out Assembly assembly)
-        {
-            if (File.Exists(filePath))
-            {
-                AssemblyName name = AssemblyName.GetAssemblyName(filePath);
-                if (name.Version >= minimumVersion)
-                {
-                    assembly = Assembly.LoadFile(filePath);
-                    return true;
-                }
-            }
-            assembly = null;
-            return false;
-        }
-    }
-}
index 0799b08a02427443abac7eb5a68298ac788a9172..900f6f43ca2e9040e6580c9cbaf9ff056eaea923 100644 (file)
@@ -208,6 +208,7 @@ namespace SOS.Extensions
 
                 // Display any extension assembly loads on console
                 _serviceManager.NotifyExtensionLoad.Register((Assembly assembly) => fileLoggingConsoleService.WriteLine($"Loading extension {assembly.Location}"));
+                _serviceManager.NotifyExtensionLoadFailure.Register((Exception ex) => fileLoggingConsoleService.WriteLine(ex.Message));
 
                 // Load any extra extensions in the search path
                 _serviceManager.LoadExtensions();
index 2a0c765e97914e1818f510b2b0b05b0ef6ace5da..6718677d65cfce640cec490cb98d6535f3c90fc2 100644 (file)
@@ -91,6 +91,7 @@ namespace Microsoft.Diagnostics.Tools.Dump
 
             // Display any extension assembly loads on console
             _serviceManager.NotifyExtensionLoad.Register((Assembly assembly) => _fileLoggingConsoleService.WriteLine($"Loading extension {assembly.Location}"));
+            _serviceManager.NotifyExtensionLoadFailure.Register((Exception ex) => _fileLoggingConsoleService.WriteLine(ex.Message));
 
             // Load any extra extensions
             _serviceManager.LoadExtensions();