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);