ALC enhancements (#22273)
authorSteve MacLean <stmaclea@microsoft.com>
Wed, 27 Mar 2019 23:49:17 +0000 (19:49 -0400)
committerGitHub <noreply@github.com>
Wed, 27 Mar 2019 23:49:17 +0000 (19:49 -0400)
* Initial implementatation of ALC enhancements

Add public constructor with name
Add Name property
Add Assemblies property
Add All property
Override ToString
Name Default and Assembly.Load(...) ALCs
Add AssemblyLoadContext30Extensions test

src/System.Private.CoreLib/shared/System/Reflection/Assembly.cs
src/System.Private.CoreLib/shared/System/Runtime/Loader/AssemblyLoadContext.cs
src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.CoreCLR.cs
tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.cs [new file with mode: 0644]
tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.csproj [new file with mode: 0644]

index 35772af..c54e39f 100644 (file)
@@ -227,7 +227,7 @@ namespace System.Reflection
             SerializationInfo.ThrowIfDeserializationInProgress("AllowAssembliesFromByteArrays",
                 ref s_cachedSerializationSwitch);
 
-            AssemblyLoadContext alc = new IndividualAssemblyLoadContext();
+            AssemblyLoadContext alc = new IndividualAssemblyLoadContext("Assembly.Load(byte[], ...)");
             return alc.InternalLoad(rawAssembly, rawSymbolStore);
         }
 
@@ -254,7 +254,7 @@ namespace System.Reflection
                 if (s_loadfile.TryGetValue(normalizedPath, out result))
                     return result;
 
-                AssemblyLoadContext alc = new IndividualAssemblyLoadContext();
+                AssemblyLoadContext alc = new IndividualAssemblyLoadContext(String.Format("Assembly.LoadFile({0})", path));
                 result = alc.LoadFromAssemblyPath(normalizedPath);
                 s_loadfile.Add(normalizedPath, result);
             }
index 3bf2e3e..4be5218 100644 (file)
@@ -12,7 +12,7 @@ using System.Threading;
 
 namespace System.Runtime.Loader
 {
-    public abstract partial class AssemblyLoadContext
+    public partial class AssemblyLoadContext
     {
         private enum InternalState
         {
@@ -44,18 +44,25 @@ namespace System.Runtime.Loader
         // Contains the reference to VM's representation of the AssemblyLoadContext
         private readonly IntPtr _nativeAssemblyLoadContext;
 
-        protected AssemblyLoadContext() : this(false, false)
+        protected AssemblyLoadContext() : this(false, false, null)
         {
         }
 
-        protected AssemblyLoadContext(bool isCollectible) : this(false, isCollectible)
+        protected AssemblyLoadContext(bool isCollectible) : this(false, isCollectible, null)
         {
         }
 
-        private protected AssemblyLoadContext(bool representsTPALoadContext, bool isCollectible)
+        public AssemblyLoadContext(string name, bool isCollectible = true) : this(false, isCollectible, name)
+        {
+        }
+
+        private protected AssemblyLoadContext(bool representsTPALoadContext, bool isCollectible, string name)
         {
             // Initialize the VM side of AssemblyLoadContext if not already done.
             IsCollectible = isCollectible;
+
+            Name = name;
+
             // The _unloadLock needs to be assigned after the IsCollectible to ensure proper behavior of the finalizer
             // even in case the following allocation fails or the thread is aborted between these two lines.
             _unloadLock = new object();
@@ -132,6 +139,22 @@ namespace System.Runtime.Loader
             }
         }
 
+        public IEnumerable<Assembly> Assemblies
+        {
+            get
+            {
+                foreach (Assembly a in GetLoadedAssemblies())
+                {
+                    AssemblyLoadContext alc = GetLoadContext(a);
+
+                    if (alc == this)
+                    {
+                        yield return a;
+                    }
+                }
+            }
+        }
+
         // Event handler for resolving native libraries.
         // This event is raised if the native library could not be resolved via
         // the default resolution logic [including AssemblyLoadContext.LoadUnmanagedDll()]
@@ -167,6 +190,37 @@ namespace System.Runtime.Loader
 
         public bool IsCollectible { get; }
 
+        public string Name { get; }
+
+        public override string ToString() => "\"" + Name + "\" " + GetType().ToString() + " #" + _id;
+
+        public static IEnumerable<AssemblyLoadContext> All
+        {
+            get
+            {
+                AssemblyLoadContext d = AssemblyLoadContext.Default; // Ensure default is initialized
+
+                List<WeakReference<AssemblyLoadContext>> alcList = null;
+                lock (s_contextsToUnload)
+                {
+                    // To make this thread safe we need a quick snapshot while locked
+                    alcList = new List<WeakReference<AssemblyLoadContext>>(s_contextsToUnload.Values);
+                }
+
+                foreach (WeakReference<AssemblyLoadContext> weakAlc in alcList)
+                {
+                    AssemblyLoadContext alc = null;
+
+                    weakAlc.TryGetTarget(out alc);
+
+                    if (alc != null)
+                    {
+                        yield return alc;
+                    }
+                }
+            }
+        }
+
         // Helper to return AssemblyName corresponding to the path of an IL assembly
         public static AssemblyName GetAssemblyName(string assemblyPath)
         {
@@ -181,7 +235,10 @@ namespace System.Runtime.Loader
         // Custom AssemblyLoadContext implementations can override this
         // method to perform custom processing and use one of the protected
         // helpers above to load the assembly.
-        protected abstract Assembly Load(AssemblyName assemblyName);
+        protected virtual Assembly Load(AssemblyName assemblyName)
+        {
+            return null;
+        }
 
         [System.Security.DynamicSecurityMethod] // Methods containing StackCrawlMark local var has to be marked DynamicSecurityMethod
         public Assembly LoadFromAssemblyName(AssemblyName assemblyName)
@@ -356,27 +413,15 @@ namespace System.Runtime.Loader
     {
         internal static readonly AssemblyLoadContext s_loadContext = new DefaultAssemblyLoadContext();
 
-        internal DefaultAssemblyLoadContext() : base(true, false)
+        internal DefaultAssemblyLoadContext() : base(true, false, "Default")
         {
         }
-
-        protected override Assembly Load(AssemblyName assemblyName)
-        {
-            // We were loading an assembly into TPA ALC that was not found on TPA list. As a result we are here.
-            // Returning null will result in the AssemblyResolve event subscribers to be invoked to help resolve the assembly.
-            return null;
-        }
     }
 
     internal sealed class IndividualAssemblyLoadContext : AssemblyLoadContext
     {
-        internal IndividualAssemblyLoadContext() : base(false, false)
+        internal IndividualAssemblyLoadContext(String name) : base(false, false, name)
         {
         }
-
-        protected override Assembly Load(AssemblyName assemblyName)
-        {
-            return null;
-        }
     }
-}
\ No newline at end of file
+}
index b36c382..61bef8e 100644 (file)
@@ -10,7 +10,7 @@ using System.Runtime.InteropServices;
 
 namespace System.Runtime.Loader
 {
-    public abstract partial class AssemblyLoadContext
+    public partial class AssemblyLoadContext
     {
         [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
         private static extern IntPtr InitializeAssemblyLoadContext(IntPtr ptrAssemblyLoadContext, bool fRepresentsTPALoadContext, bool isCollectible);
diff --git a/tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.cs b/tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.cs
new file mode 100644 (file)
index 0000000..2d2a9d8
--- /dev/null
@@ -0,0 +1,243 @@
+// 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.Reflection;
+using System.Runtime.Loader;
+using System.IO;
+
+using Console = Internal.Console;
+
+namespace My
+{
+    public class CustomAssemblyLoadContext : AssemblyLoadContext
+    {
+        public CustomAssemblyLoadContext() : base()
+        {
+        }
+    }
+}
+
+public class Program
+{
+    private static bool passed = true;
+
+    public static void Assert(bool value, string message = "none")
+    {
+        if (!value)
+        {
+            Console.WriteLine("FAIL! " + message);
+            passed = false;
+        }
+    }
+
+    public static bool AssembliesContainAssembly(AssemblyLoadContext alc, Assembly a)
+    {
+        foreach (Assembly b in alc.Assemblies)
+        {
+            if (a == b)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static bool PropertyAllContainsContext(AssemblyLoadContext alc)
+    {
+        foreach (AssemblyLoadContext c in AssemblyLoadContext.All)
+        {
+            if (alc == c)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static void DefaultName()
+    {
+        try
+        {
+            Console.WriteLine("DefaultName()");
+
+            AssemblyLoadContext alc = AssemblyLoadContext.Default;
+            Assert(alc == AssemblyLoadContext.GetLoadContext(typeof(Program).Assembly));
+            Assert(PropertyAllContainsContext(alc));
+            Assert(AssembliesContainAssembly(alc, typeof(Program).Assembly));
+
+            Console.WriteLine(alc.Name);
+            Assert(alc.Name == "Default");
+
+            Console.WriteLine(alc.GetType().ToString());
+            Assert(alc.GetType().ToString() == "System.Runtime.Loader.DefaultAssemblyLoadContext");
+
+            Console.WriteLine(alc.ToString());
+            Assert(alc.ToString().Contains("\"Default"));
+            Assert(alc.ToString().Contains("\" System.Runtime.Loader.DefaultAssemblyLoadContext"));
+            Assert(alc.ToString().Contains(" #"));
+        }
+        catch (Exception e)
+        {
+            Assert(false, e.ToString());
+        }
+    }
+
+    public static void AssemblyLoadFileName()
+    {
+        try
+        {
+            Console.WriteLine("AssemblyLoadFileName()");
+
+            String path = typeof(Program).Assembly.Location;
+            Assembly a = Assembly.LoadFile(path);
+
+            Assert(a != typeof(Program).Assembly);
+
+            AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(a);
+            Assert(PropertyAllContainsContext(alc));
+            Assert(AssembliesContainAssembly(alc, a));
+            Assert(alc != AssemblyLoadContext.Default);
+
+
+            Console.WriteLine(alc.Name);
+            Assert(alc.Name == String.Format("Assembly.LoadFile({0})", path));
+
+            Console.WriteLine(alc.GetType().ToString());
+            Assert(alc.GetType().ToString() == "System.Runtime.Loader.IndividualAssemblyLoadContext");
+
+            Console.WriteLine(alc.ToString());
+            Assert(alc.ToString().Contains("\"" + String.Format("Assembly.LoadFile({0})", path)));
+            Assert(alc.ToString().Contains("\" System.Runtime.Loader.IndividualAssemblyLoadContext"));
+            Assert(alc.ToString().Contains(" #"));
+        }
+        catch (Exception e)
+        {
+            Assert(false, e.ToString());
+        }
+    }
+
+    public static void AssemblyLoadByteArrayName()
+    {
+#if ReadAllBytes // System.IO.File.ReadAllBytes is not found when ReferenceSystemPrivateCoreLib is true
+        try
+        {
+            Console.WriteLine("AssemblyLoadByteArrayName()");
+
+            String path = typeof(Program).Assembly.Location;
+            Byte [] byteArray = System.IO.File.ReadAllBytes(path);
+            Assembly a = Assembly.Load(byteArray);
+            AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(a);
+            Assert(PropertyAllContainsContext(alc));
+            Assert(AssembliesContainAssembly(alc, a));
+
+            Console.WriteLine(alc.Name);
+            Assert(alc.Name == "Assembly.Load(byte[], ...)");
+
+            Console.WriteLine(alc.GetType().ToString());
+            Assert(alc.GetType().ToString() == "System.Runtime.Loader.IndividualAssemblyLoadContext");
+
+            Console.WriteLine(alc.ToString());
+            Assert(alc.ToString().Contains("\"Default"));
+            Assert(alc.ToString().Contains("\" System.Runtime.Loader.IndividualAssemblyLoadContext"));
+            Assert(alc.ToString().Contains(" #"));
+        }
+        catch (Exception e)
+        {
+            Assert(false, e.ToString());
+        }
+#endif
+    }
+
+    public static void CustomWOName()
+    {
+        try
+        {
+            Console.WriteLine("CustomWOName()");
+
+            // ALC should be a concrete class
+            AssemblyLoadContext alc = new My.CustomAssemblyLoadContext();
+            Assert(PropertyAllContainsContext(alc));
+
+            Console.WriteLine(alc.Name);
+            Assert(alc.Name == null);
+
+            Console.WriteLine(alc.GetType().ToString());
+            Assert(alc.GetType().ToString() == "My.CustomAssemblyLoadContext");
+
+            Console.WriteLine(alc.ToString());
+            Assert(alc.ToString().Contains("\"\" "));
+            Assert(alc.ToString().Contains("\" My.CustomAssemblyLoadContext"));
+            Assert(alc.ToString().Contains(" #"));
+        }
+        catch (Exception e)
+        {
+            Assert(false, e.ToString());
+        }
+    }
+
+    public static void CustomName()
+    {
+        try
+        {
+            Console.WriteLine("CustomName()");
+
+            // ALC should be a concrete class
+            AssemblyLoadContext alc = new AssemblyLoadContext("CustomName");
+            Assert(PropertyAllContainsContext(alc));
+
+            Console.WriteLine(alc.Name);
+            Assert(alc.Name == "CustomName");
+
+            Console.WriteLine(alc.GetType().ToString());
+            Assert(alc.GetType().ToString() == "System.Runtime.Loader.AssemblyLoadContext");
+
+            Console.WriteLine(alc.ToString());
+            Assert(alc.ToString().Contains("\"CustomName"));
+            Assert(alc.ToString().Contains("\" System.Runtime.Loader.AssemblyLoadContext"));
+            Assert(alc.ToString().Contains(" #"));
+        }
+        catch (Exception e)
+        {
+            Assert(false, e.ToString());
+        }
+    }
+
+    public static int Main()
+    {
+        foreach (AssemblyLoadContext alc in AssemblyLoadContext.All)
+        {
+            Console.WriteLine(alc.ToString());
+            foreach (Assembly a in alc.Assemblies)
+            {
+                Console.WriteLine(a.ToString());
+            }
+        }
+
+        DefaultName();
+        AssemblyLoadFileName();
+        AssemblyLoadByteArrayName();
+        CustomWOName();
+        CustomName();
+
+        foreach (AssemblyLoadContext alc in AssemblyLoadContext.All)
+        {
+            Console.WriteLine(alc.ToString());
+            foreach (Assembly a in alc.Assemblies)
+            {
+                Console.WriteLine(a.ToString());
+            }
+        }
+
+        if (passed)
+        {
+            Console.WriteLine("Test PASSED!");
+            return 100;
+        }
+        else
+        {
+            Console.WriteLine("Test FAILED!");
+            return -1;
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.csproj b/tests/src/Loader/AssemblyLoadContext30Extensions/AssemblyLoadContext30Extensions.csproj
new file mode 100644 (file)
index 0000000..5ef86a1
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <AssemblyName>AssemblyLoadContext30Extensions</AssemblyName>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{95DFC527-4DC1-495E-97D7-E94EE1F7140D}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+    <ReferenceSystemPrivateCoreLib>true</ReferenceSystemPrivateCoreLib>
+  </PropertyGroup>
+  <!-- Default configurations to help VS understand the configurations -->
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
+  </PropertyGroup>
+  <ItemGroup>
+    <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+      <Visible>False</Visible>
+    </CodeAnalysisDependentAssemblyPaths>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="AssemblyLoadContext30Extensions.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
+  </ItemGroup>
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project>