diff --git a/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/ManagedProfilerAssemblyResolver.NetFramework.cs b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/ManagedProfilerAssemblyResolver.NetFramework.cs new file mode 100644 index 000000000000..5d8639bc47ca --- /dev/null +++ b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/ManagedProfilerAssemblyResolver.NetFramework.cs @@ -0,0 +1,81 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if NETFRAMEWORK + +#nullable enable + +using System; +using System.IO; +using System.Reflection; + +namespace Datadog.Trace.ClrProfiler.Managed.Loader; + +// Owns the AppDomain.AssemblyResolve callback on .NET Framework. Kept on a +// separate class from Startup so the handler can be dispatched without +// forcing Startup's type-initializer to have finished. If the handler were +// a method on Startup, a configBuilder attached to that +// issues sync-over-async work during Startup..cctor could deadlock: the +// main thread would be blocked inside the .cctor waiting for a Task, whose +// continuation runs on a ThreadPool thread that probes Type.GetType and +// fires AssemblyResolve, whose handler would then wait on Startup..cctor. +internal static class ManagedProfilerAssemblyResolver +{ + // Seeded by Startup..cctor before the handler is subscribed. + internal static string? ManagedProfilerDirectory { get; set; } + + internal static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs args) + { + try + { + return ResolveAssembly(args.Name); + } + catch (Exception ex) + { + StartupLogger.Log(ex, "Error resolving assembly: {0}", args.Name); + } + + return null; + } + + internal static Assembly? ResolveAssembly(string name) + { + var assemblyName = new AssemblyName(name); + + // On .NET Framework, having a non-US locale can cause mscorlib + // to enter the AssemblyResolve event when searching for resources + // in its satellite assemblies. Exit early so we don't cause + // infinite recursion. + if (string.Equals(assemblyName.Name, "mscorlib.resources", StringComparison.OrdinalIgnoreCase) || + string.Equals(assemblyName.Name, "System.Net.Http", StringComparison.OrdinalIgnoreCase) || + string.Equals(assemblyName.Name, "vstest.console.resources", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // WARNING: Logs must not be added _before_ we check for the above bail-out conditions + var path = string.IsNullOrEmpty(ManagedProfilerDirectory) ? $"{assemblyName.Name}.dll" : Path.Combine(ManagedProfilerDirectory, $"{assemblyName.Name}.dll"); + StartupLogger.Debug("Assembly Resolve event received for: {0}. Looking for: {1}", name, path); + + if (File.Exists(path)) + { + if (name.StartsWith("Datadog.Trace, Version=", StringComparison.Ordinal) && name != Startup.AssemblyName) + { + StartupLogger.Debug(" Trying to load '{0}' which does not match the expected version ('{1}'). [Path={2}]", name, Startup.AssemblyName, path); + return null; + } + + StartupLogger.Debug("Calling Assembly.LoadFrom(\"{0}\")", path); + var assembly = Assembly.LoadFrom(path); + StartupLogger.Debug("Assembly loaded: {0}", assembly.FullName); + return assembly; + } + + StartupLogger.Debug("Assembly not found in path: {0}", path); + return null; + } +} + +#endif diff --git a/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetFramework.cs b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetFramework.cs index 6668188c9084..5d669459eae9 100644 --- a/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetFramework.cs +++ b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetFramework.cs @@ -7,9 +7,7 @@ #nullable enable -using System; using System.IO; -using System.Reflection; namespace Datadog.Trace.ClrProfiler.Managed.Loader { @@ -22,57 +20,6 @@ internal static string ComputeTfmDirectory(string tracerHomeDirectory) { return Path.Combine(Path.GetFullPath(tracerHomeDirectory), "net461"); } - - private static Assembly? AssemblyResolve_ManagedProfilerDependencies(object sender, ResolveEventArgs args) - { - try - { - return ResolveAssembly(args.Name); - } - catch (Exception ex) - { - StartupLogger.Log(ex, "Error resolving assembly: {0}", args.Name); - } - - return null; - } - - private static Assembly? ResolveAssembly(string name) - { - var assemblyName = new AssemblyName(name); - - // On .NET Framework, having a non-US locale can cause mscorlib - // to enter the AssemblyResolve event when searching for resources - // in its satellite assemblies. Exit early so we don't cause - // infinite recursion. - if (string.Equals(assemblyName.Name, "mscorlib.resources", StringComparison.OrdinalIgnoreCase) || - string.Equals(assemblyName.Name, "System.Net.Http", StringComparison.OrdinalIgnoreCase) || - string.Equals(assemblyName.Name, "vstest.console.resources", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - // WARNING: Logs must not be added _before_ we check for the above bail-out conditions - var path = string.IsNullOrEmpty(ManagedProfilerDirectory) ? $"{assemblyName.Name}.dll" : Path.Combine(ManagedProfilerDirectory, $"{assemblyName.Name}.dll"); - StartupLogger.Debug("Assembly Resolve event received for: {0}. Looking for: {1}", name, path); - - if (File.Exists(path)) - { - if (name.StartsWith("Datadog.Trace, Version=", StringComparison.Ordinal) && name != AssemblyName) - { - StartupLogger.Debug(" Trying to load '{0}' which does not match the expected version ('{1}'). [Path={2}]", name, AssemblyName, path); - return null; - } - - StartupLogger.Debug("Calling Assembly.LoadFrom(\"{0}\")", path); - var assembly = Assembly.LoadFrom(path); - StartupLogger.Debug("Assembly loaded: {0}", assembly.FullName); - return assembly; - } - - StartupLogger.Debug("Assembly not found in path: {0}", path); - return null; - } } } diff --git a/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs index d203cb69f34a..9f7a8a6a8336 100644 --- a/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs +++ b/tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs @@ -17,7 +17,9 @@ namespace Datadog.Trace.ClrProfiler.Managed.Loader /// public sealed partial class Startup { - private const string AssemblyName = "Datadog.Trace, Version=3.43.0.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb"; + // internal so ManagedProfilerAssemblyResolver can reference it. Safe because const strings are inlined at compile time. + // Do not add non-const static members on Startup that the resolver needs - that would re-introduce the .cctor deadlock. + internal const string AssemblyName = "Datadog.Trace, Version=3.43.0.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb"; private const string AzureAppServicesSiteExtensionKey = "DD_AZURE_APP_SERVICES"; // only set when using the AAS site extension private const string TracerHomePathKey = "DD_DOTNET_TRACER_HOME"; @@ -78,7 +80,17 @@ static Startup() try { +#if NETFRAMEWORK + // On .NET Framework, route AssemblyResolve through a class other than Startup so + // the handler doesn't require Startup..cctor to have finished. If a configBuilder + // on issues sync-over-async work during Startup..cctor, the async + // continuation may fire AssemblyResolve on a ThreadPool thread; a handler on + // Startup itself would deadlock waiting on Startup..cctor. + ManagedProfilerAssemblyResolver.ManagedProfilerDirectory = ManagedProfilerDirectory; + AppDomain.CurrentDomain.AssemblyResolve += ManagedProfilerAssemblyResolver.OnAssemblyResolve; +#else AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve_ManagedProfilerDependencies; +#endif } catch (Exception ex) { @@ -181,7 +193,11 @@ private static void TryInvokeManagedMethod(string typeName, string methodName, s // We will try to resolve it manually as a last chance. StartupLogger.Log(ex, "Error on assembly load: {0}, Trying to solve it manually...", assemblyString); +#if NETFRAMEWORK + var assembly = ManagedProfilerAssemblyResolver.ResolveAssembly(assemblyString); +#else var assembly = ResolveAssembly(assemblyString); +#endif if (assembly is not null) { StartupLogger.Log("Assembly '{0}' was resolved manually.", assemblyString);