From 79ec659a452ac87aaf1d2ac28b27da8065ab3d25 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 10 Jan 2024 16:13:38 -0500 Subject: [PATCH] Add host option to disable AssemblyLoadContext A host can disable the use of AssemblyLoadContext by returning a value of "true" or "1" when GetHostOption is called with the option name "DisableAssemblyLoadContext". This is an escape hatch for hosts that have issues with the ALC behavior, allowing them to opt out until the underlying issues can be fixed. --- .../CompiledAssemblyData.cs | 3 +- ...ompiledTemplate.TemplateAssemblyContext.cs | 54 ++++++++++++++----- .../CompiledTemplate.TemplateExecutor.cs | 2 +- .../CurrentDomainAssemblyResolver.cs | 4 -- .../Mono.TextTemplating/HostOptions.cs | 16 ++++++ 5 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 Mono.TextTemplating/Mono.TextTemplating/HostOptions.cs diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs index 98f24458..a18665a7 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs @@ -38,7 +38,7 @@ public Assembly LoadInAssemblyLoadContext (AssemblyLoadContext loadContext) return loadContext.LoadFromStream (new MemoryStream (Assembly)); } } -#else +#endif public Assembly LoadInCurrentAppDomain () { if (DebugSymbols != null) { @@ -47,6 +47,5 @@ public Assembly LoadInCurrentAppDomain () return System.Reflection.Assembly.Load (Assembly); } } -#endif } } diff --git a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs index 0dba4fc4..177d37f4 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs @@ -11,21 +11,51 @@ namespace Mono.TextTemplating; partial class CompiledTemplate { - [SuppressMessage ("Performance", "CA1822:Mark members as static", Justification = "Same API for ALC and AppDomain build variants of the class")] - sealed class TemplateAssemblyContext : IDisposable + /// + /// Abstracts over loading assemblies into an AssemblyLoadContext or AppDomain + /// and resolving assemblies from the host. + /// + abstract class TemplateAssemblyContext : IDisposable { + public abstract Assembly LoadAssemblyFile (string assemblyPath); + public abstract Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData); + public virtual void Dispose () { } + + [SuppressMessage ("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Conditionally compiled version of this returns multiple concrete types")] + + public static TemplateAssemblyContext Create (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) + { +#if FEATURE_ASSEMBLY_LOAD_CONTEXT + if (!host.IsAssemblyLoadContextDisabled ()) { + return new AssemblyLoadContextTemplateAssemblyContext (host, referenceAssemblyFiles); + } +#endif + return new CurrentAppDomainTemplateAssemblyContext (host, referenceAssemblyFiles); + } + } + #if FEATURE_ASSEMBLY_LOAD_CONTEXT + sealed class AssemblyLoadContextTemplateAssemblyContext : TemplateAssemblyContext + { readonly TemplateAssemblyLoadContext templateContext; - public TemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) => templateContext = new (referenceAssemblyFiles, host); - public Assembly LoadAssemblyFile (string assemblyPath) => templateContext.LoadFromAssemblyPath (assemblyPath); - public Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInAssemblyLoadContext (templateContext); - public void Dispose () { } -#else - readonly CurrentDomainAssemblyResolver assemblyResolver; - public TemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) => assemblyResolver = new (referenceAssemblyFiles, host.ResolveAssemblyReference); - public Assembly LoadAssemblyFile (string assemblyPath) => Assembly.LoadFile (assemblyPath); - public Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInCurrentAppDomain (); - public void Dispose () => assemblyResolver.Dispose (); + public AssemblyLoadContextTemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) + => templateContext = new (referenceAssemblyFiles, host); + public override Assembly LoadAssemblyFile (string assemblyPath) => templateContext.LoadFromAssemblyPath (assemblyPath); + public override Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInAssemblyLoadContext (templateContext); + } #endif + + sealed class CurrentAppDomainTemplateAssemblyContext : TemplateAssemblyContext + { + readonly CurrentDomainAssemblyResolver assemblyResolver; + public CurrentAppDomainTemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) + => assemblyResolver = new (referenceAssemblyFiles, host.ResolveAssemblyReference); + public override Assembly LoadAssemblyFile (string assemblyPath) => Assembly.LoadFile (assemblyPath); + public override Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInCurrentAppDomain (); + public override void Dispose () + { + base.Dispose (); + assemblyResolver.Dispose (); + } } } diff --git a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs index 8fc8a467..3a40b77b 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs @@ -19,7 +19,7 @@ class TemplateProcessor : MarshalByRefObject [SuppressMessage ("Performance", "CA1822:Mark members as static", Justification = "Needs to be an instance for MarshalByRefObject")] public string CreateAndProcess (ITextTemplatingEngineHost host, CompiledAssemblyData templateAssemblyData, string templateAssemblyFile, string fullName, CultureInfo culture, string[] referencedAssemblyFiles) { - using var context = new TemplateAssemblyContext (host, referencedAssemblyFiles); + using var context = TemplateAssemblyContext.Create (host, referencedAssemblyFiles); Assembly assembly = templateAssemblyData is not null ? context.LoadInMemoryAssembly (templateAssemblyData) diff --git a/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs b/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs index 2d79a8a0..e8a9549a 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if !FEATURE_ASSEMBLY_LOAD_CONTEXT - using System; using System.IO; using System.Reflection; @@ -49,5 +47,3 @@ public void Dispose () } } } - -#endif \ No newline at end of file diff --git a/Mono.TextTemplating/Mono.TextTemplating/HostOptions.cs b/Mono.TextTemplating/Mono.TextTemplating/HostOptions.cs new file mode 100644 index 00000000..ee43096a --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating/HostOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.VisualStudio.TextTemplating; + +static class HostOptionExtensions +{ + const string DisableAlcOptionName = "DisableAssemblyLoadContext"; + + static bool IsOptionTrue (this ITextTemplatingEngineHost host, string optionName) => + host.GetHostOption(optionName) is string optionVal + && (optionVal == "1" || optionVal.Equals("true", StringComparison.OrdinalIgnoreCase)); + + public static bool IsAssemblyLoadContextDisabled (this ITextTemplatingEngineHost host) => host.IsOptionTrue (DisableAlcOptionName); +} \ No newline at end of file