File: BackEnd\Components\RequestBuilder\AssemblyLoadsTracker.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
#if FEATURE_APPDOMAIN
using System.Collections.Generic;
using System.Linq;
#endif
using System.Reflection;
#if FEATURE_ASSEMBLYLOADCONTEXT
using System.Runtime.Loader;
#endif
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Framework;
 
namespace Microsoft.Build.BackEnd.Components.RequestBuilder
{
    internal sealed class AssemblyLoadsTracker : MarshalByRefObject, IDisposable
    {
#if FEATURE_APPDOMAIN
        private static readonly List<AssemblyLoadsTracker> s_instances = new();
#endif
        private readonly LoggingContext? _loggingContext;
        private readonly LoggingService? _loggingService;
        private readonly AssemblyLoadingContext _context;
        private readonly string? _initiator;
        private readonly AppDomain _appDomain;
 
        private AssemblyLoadsTracker(
            LoggingContext? loggingContext,
            LoggingService? loggingService,
            AssemblyLoadingContext context,
            Type? initiator,
            AppDomain appDomain)
        {
            _loggingContext = loggingContext;
            _loggingService = loggingService;
            _context = context;
            _initiator = initiator?.FullName;
            _appDomain = appDomain;
        }
 
        public static IDisposable StartTracking(
            LoggingContext loggingContext,
            AssemblyLoadingContext context,
            Type? initiator,
            AppDomain? appDomain = null)
            => StartTracking(loggingContext, null, context, initiator, null, appDomain);
 
        public static IDisposable StartTracking(
            LoggingContext loggingContext,
            AssemblyLoadingContext context,
            string? initiator = null,
            AppDomain? appDomain = null)
            => StartTracking(loggingContext, null, context, null, initiator, appDomain);
 
        public static IDisposable StartTracking(
            LoggingService loggingService,
            AssemblyLoadingContext context,
            Type initiator,
            AppDomain? appDomain = null)
            => StartTracking(null, loggingService, context, initiator, null, appDomain);
 
 
 
#if FEATURE_APPDOMAIN
        public static void StopTracking(AppDomain appDomain)
        {
            if (!appDomain.IsDefaultAppDomain())
            {
                lock (s_instances)
                {
                    foreach (AssemblyLoadsTracker tracker in s_instances.Where(t => t._appDomain == appDomain))
                    {
                        tracker.StopTracking();
                    }
 
                    s_instances.RemoveAll(t => t._appDomain == appDomain);
                }
            }
        }
#endif
 
        public void Dispose()
        {
            StopTracking();
        }
 
        private static bool IsBuiltinType(string? typeName)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                return false;
            }
 
            return typeName!.StartsWith("Microsoft.Build", StringComparison.Ordinal) ||
                   typeName.StartsWith("Microsoft.NET.Build", StringComparison.Ordinal) ||
                   typeName.StartsWith("Microsoft.NET.Sdk", StringComparison.Ordinal);
        }
 
        private static IDisposable StartTracking(
            LoggingContext? loggingContext,
            LoggingService? loggingService,
            AssemblyLoadingContext context,
            Type? initiatorType,
            string? initiatorName,
            AppDomain? appDomain)
        {
            if (// We do not want to load all assembly loads (including those triggered by builtin types)
                !Traits.Instance.LogAllAssemblyLoads &&
                (
                    // Load will be initiated by internal type - so we are not interested in those
                    initiatorType?.Assembly == Assembly.GetExecutingAssembly()
                    ||
                    IsBuiltinType(initiatorType?.FullName)
                    ||
                    IsBuiltinType(initiatorName)
                )
            )
            {
                return EmptyDisposable.Instance;
            }
 
            var tracker = new AssemblyLoadsTracker(loggingContext, loggingService, context, initiatorType, appDomain ?? AppDomain.CurrentDomain);
#if FEATURE_APPDOMAIN
            if (appDomain != null && !appDomain.IsDefaultAppDomain())
            {
                lock (s_instances)
                {
                    s_instances.Add(tracker);
                }
            }
#endif
            tracker.StartTracking();
            return tracker;
        }
 
        private void StartTracking()
        {
            _appDomain.AssemblyLoad += CurrentDomainOnAssemblyLoad;
        }
 
        private void StopTracking()
        {
            _appDomain.AssemblyLoad -= CurrentDomainOnAssemblyLoad;
        }
 
        private void CurrentDomainOnAssemblyLoad(object? sender, AssemblyLoadEventArgs args)
        {
            string? assemblyName = args.LoadedAssembly.FullName;
            string assemblyPath = args.LoadedAssembly.IsDynamic ? string.Empty : args.LoadedAssembly.Location;
            Guid mvid = args.LoadedAssembly.ManifestModule.ModuleVersionId;
#if FEATURE_ASSEMBLYLOADCONTEXT
            // AssemblyLoadContext.GetLoadContext returns null when the assembly isn't a RuntimeAssembly, which should not be the case here.
            // Name would only be null if the AssemblyLoadContext didn't supply a name, but MSBuildLoadContext does.
            string appDomainDescriptor = AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)?.Name ?? "Unknown";
#else
            string? appDomainDescriptor = _appDomain.IsDefaultAppDomain()
                ? null
                : $"{_appDomain.Id}|{_appDomain.FriendlyName}";
#endif
 
            AssemblyLoadBuildEventArgs buildArgs = new(_context, _initiator, assemblyName, assemblyPath, mvid, appDomainDescriptor);
 
            // Fix #8816 - when LoggingContext does not have BuildEventContext it is unable to log anything
            if (_loggingContext?.BuildEventContext != null)
            {
                buildArgs.BuildEventContext = _loggingContext.BuildEventContext;
                // bypass the logging context validity check: it's possible that the load happened
                // on a thread unrelated to the context we're tracking loads in
                _loggingContext.LoggingService.LogBuildEvent(buildArgs);
            }
            _loggingService?.LogBuildEvent(buildArgs);
        }
 
        private class EmptyDisposable : IDisposable
        {
            public static readonly IDisposable Instance = new EmptyDisposable();
            public void Dispose() { }
        }
    }
}