Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// <copyright file="ManagedProfilerAssemblyResolver.NetCore.cs" company="Datadog">
// 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.
// </copyright>

#if NETCOREAPP

#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace Datadog.Trace.ClrProfiler.Managed.Loader;

// Owns the AppDomain.AssemblyResolve and AssemblyLoadContext.Default.Resolving
// callbacks on .NET Core. Kept on a separate class from Startup so the handlers
// can be dispatched without forcing Startup's type-initializer to have finished -
// the same defense applied to the .NET Framework path. The Framework-specific
// configBuilder trigger doesn't exist on .NET Core, but any sync-over-async work
// in the Startup..cctor chain whose continuation probes Type.GetType or the ALC
// on a ThreadPool thread would hit the same .cctor x sync-over-async hazard.
internal static class ManagedProfilerAssemblyResolver
{
private static readonly AssemblyLoadContext DependencyLoadContext = new ManagedProfilerAssemblyLoadContext();

private static CachedAssembly[]? _assemblies;

// Seeded by Startup..cctor before the handlers are subscribed.
internal static string? ManagedProfilerDirectory { get; set; }

internal static void PopulateAssemblyCache(string directory)
{
if (!Directory.Exists(directory))
{
return;
}

// List/Array due to the number of files in the tracer home folder
// (7 in netstandard, 2 netcoreapp3.1+)
var assemblies = new List<CachedAssembly>();
foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly))
{
assemblies.Add(new CachedAssembly(file, null));
}

_assemblies = [..assemblies];
StartupLogger.Debug("Total number of assemblies: {0}", _assemblies.Length);
}

internal static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs args)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is just a cut-and paste from Startup.NetCore, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost. It was, but I noticed that the original code had no try/catch (the .net framework version does) so these were added.

{
try
{
return ResolveAssembly(args.Name);
}
catch (Exception ex)
{
StartupLogger.Log(ex, "Error resolving assembly: {0}", args.Name);
}

return null;
}

internal static Assembly? OnAssemblyLoadContextResolving(AssemblyLoadContext context, AssemblyName assemblyName)
{
try
{
return ResolveAssembly(assemblyName.Name);
}
catch (Exception ex)
{
StartupLogger.Log(ex, "Error resolving assembly: {0}", assemblyName.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. This seems to have been fixed in
// .NET Core in the 2.0 servicing branch, so we should not see this
// occur but guard against it anyway. If we do see it, exit early
// so we don't cause infinite recursion.
if (string.Equals(assemblyName.Name, "System.Private.CoreLib.resources", StringComparison.OrdinalIgnoreCase) ||
string.Equals(assemblyName.Name, "System.Net.Http", StringComparison.OrdinalIgnoreCase))
{
return null;
}

// WARNING: Logs must not be added _before_ we check for the above bail-out conditions
StartupLogger.Debug("Assembly Resolve event received for: {0}. Searching in: {1}", name, ManagedProfilerDirectory);
var path = Path.Combine(ManagedProfilerDirectory ?? string.Empty, $"{assemblyName.Name}.dll");

if (IsDatadogAssembly(path, out var cachedAssembly))
{
// The file exists in the Home folder...
if (cachedAssembly is not null)
{
// The assembly is already loaded.
StartupLogger.Debug("Loading from cache. [Path: {0}]", path);
return cachedAssembly;
}

// Only load the main profiler into the default AssemblyLoadContext.
// If the NuGet package provides Datadog.Trace or other libraries, loading them is handled in the following two ways:
// 1) If the AssemblyVersion is greater than or equal to the version used by Datadog.Trace, the assembly
// will load successfully and will not invoke this resolve event.
// 2) If the AssemblyVersion is lower than the version used by Datadog.Trace, the assembly will fail to load
// and invoke this resolve event. It must be loaded in a separate AssemblyLoadContext since the application will only
// load the originally referenced version.
StartupLogger.Debug("Calling DependencyLoadContext.LoadFromAssemblyPath(\"{0}\")", path);
var assembly = DependencyLoadContext.LoadFromAssemblyPath(path); // Load unresolved framework and third-party dependencies into a custom AssemblyLoadContext
SetDatadogAssembly(path, assembly);
return assembly;
}

// The file doesn't exist in the Home folder.
StartupLogger.Debug("Assembly not found in path: {0}", path);
return null;
}

private static bool IsDatadogAssembly(string path, out Assembly? cachedAssembly)
{
if (_assemblies is null)
{
cachedAssembly = null;
return false;
}

for (var i = 0; i < _assemblies.Length; i++)
{
var assembly = _assemblies[i];
if (assembly.Path == path)
{
cachedAssembly = assembly.Assembly;
return true;
}
}

cachedAssembly = null;
return false;
}

private static void SetDatadogAssembly(string path, Assembly cachedAssembly)
{
if (_assemblies is null)
{
return;
}

for (var i = 0; i < _assemblies.Length; i++)
{
if (_assemblies[i].Path == path)
{
_assemblies[i] = new CachedAssembly(path, cachedAssembly);
return;
}
}
}

private readonly struct CachedAssembly
{
public readonly string Path;
public readonly Assembly? Assembly;

public CachedAssembly(string path, Assembly? assembly)
{
Path = path;
Assembly = assembly;
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Datadog.Trace.ClrProfiler.Managed.Loader
{
Expand All @@ -19,10 +17,6 @@ namespace Datadog.Trace.ClrProfiler.Managed.Loader
/// </summary>
public partial class Startup
{
private static readonly System.Runtime.Loader.AssemblyLoadContext DependencyLoadContext = new ManagedProfilerAssemblyLoadContext();

private static CachedAssembly[]? _assemblies;

internal static string ComputeTfmDirectory(string tracerHomeDirectory)
{
var version = Environment.Version;
Expand All @@ -46,114 +40,12 @@ internal static string ComputeTfmDirectory(string tracerHomeDirectory)

var fullPath = Path.Combine(Path.GetFullPath(tracerHomeDirectory), managedLibrariesDirectory);

if (Directory.Exists(fullPath))
{
// We use the List/Array approach due to the number of files in the tracer home folder (7 in netstandard, 2 netcoreapp3.1+)
var assemblies = new List<CachedAssembly>();
foreach (var file in Directory.EnumerateFiles(fullPath, "*.dll", SearchOption.TopDirectoryOnly))
{
assemblies.Add(new CachedAssembly(file, null));
}

_assemblies = [..assemblies];
StartupLogger.Debug("Total number of assemblies: {0}", _assemblies.Length);
}
// Populate the resolver's cache. The resolver is a separate type so its handlers
// can be dispatched from ThreadPool threads without waiting on Startup..cctor.
ManagedProfilerAssemblyResolver.PopulateAssemblyCache(fullPath);

return fullPath;
}

private static Assembly? AssemblyResolve_ManagedProfilerDependencies(object sender, ResolveEventArgs args)
{
return ResolveAssembly(args.Name);
}

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. This seems to have been fixed in
// .NET Core in the 2.0 servicing branch, so we should not see this
// occur but guard against it anyway. If we do see it, exit early
// so we don't cause infinite recursion.
if (string.Equals(assemblyName.Name, "System.Private.CoreLib.resources", StringComparison.OrdinalIgnoreCase) ||
string.Equals(assemblyName.Name, "System.Net.Http", StringComparison.OrdinalIgnoreCase))
{
return null;
}

// WARNING: Logs must not be added _before_ we check for the above bail-out conditions
StartupLogger.Debug("Assembly Resolve event received for: {0}. Searching in: {1}", name, ManagedProfilerDirectory);
var path = Path.Combine(ManagedProfilerDirectory, $"{assemblyName.Name}.dll");

if (IsDatadogAssembly(path, out var cachedAssembly))
{
// The file exists in the Home folder...
if (cachedAssembly is not null)
{
// The assembly is already loaded.
StartupLogger.Debug("Loading from cache. [Path: {0}]", path);
return cachedAssembly;
}

// Only load the main profiler into the default AssemblyLoadContext.
// If the NuGet package provides Datadog.Trace or other libraries, loading them is handled in the following two ways:
// 1) If the AssemblyVersion is greater than or equal to the version used by Datadog.Trace, the assembly
// will load successfully and will not invoke this resolve event.
// 2) If the AssemblyVersion is lower than the version used by Datadog.Trace, the assembly will fail to load
// and invoke this resolve event. It must be loaded in a separate AssemblyLoadContext since the application will only
// load the originally referenced version.
StartupLogger.Debug("Calling DependencyLoadContext.LoadFromAssemblyPath(\"{0}\")", path);
var assembly = DependencyLoadContext.LoadFromAssemblyPath(path); // Load unresolved framework and third-party dependencies into a custom AssemblyLoadContext
SetDatadogAssembly(path, assembly);
return assembly;
}

// The file doesn't exist in the Home folder.
StartupLogger.Debug("Assembly not found in path: {0}", path);
return null;
}

private static bool IsDatadogAssembly(string path, out Assembly? cachedAssembly)
{
for (var i = 0; i < _assemblies!.Length; i++)
{
var assembly = _assemblies[i];
if (assembly.Path == path)
{
cachedAssembly = assembly.Assembly;
return true;
}
}

cachedAssembly = null;
return false;
}

private static void SetDatadogAssembly(string path, Assembly cachedAssembly)
{
for (var i = 0; i < _assemblies!.Length; i++)
{
if (_assemblies[i].Path == path)
{
_assemblies[i] = new CachedAssembly(path, cachedAssembly);
return;
}
}
}

private readonly struct CachedAssembly
{
public readonly string Path;
public readonly Assembly? Assembly;

public CachedAssembly(string path, Assembly? assembly)
{
Path = path;
Assembly = assembly;
}
}
}
}

Expand Down
21 changes: 7 additions & 14 deletions tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,14 @@ 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 <appSettings> 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.
// Route AssemblyResolve through a class other than Startup so the handler can
// be dispatched without Startup..cctor having finished. If any sync-over-async
// work during Startup..cctor has a continuation that fires AssemblyResolve on a
// ThreadPool thread, a handler on Startup itself would deadlock waiting on
// Startup..cctor. The observed trigger on .NET Framework is a configBuilder on
// <appSettings>; on .NET Core the same hazard applies to any equivalent pattern.
ManagedProfilerAssemblyResolver.ManagedProfilerDirectory = ManagedProfilerDirectory;
AppDomain.CurrentDomain.AssemblyResolve += ManagedProfilerAssemblyResolver.OnAssemblyResolve;
#else
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve_ManagedProfilerDependencies;
#endif
}
catch (Exception ex)
{
Expand All @@ -100,7 +97,7 @@ static Startup()
#if NETCOREAPP
try
{
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (_, assemblyName) => ResolveAssembly(assemblyName.Name);
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += ManagedProfilerAssemblyResolver.OnAssemblyLoadContextResolving;
}
catch (Exception ex)
{
Expand Down Expand Up @@ -193,11 +190,7 @@ 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);
Expand Down
Loading