// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace EpicGames.Core { /// /// Utilities for dealing with assembly loading /// public static class AssemblyUtils { /// /// Gets the original location (path and filename) of an assembly. /// This method is using Assembly.CodeBase property to properly resolve original /// assembly path in case shadow copying is enabled. /// /// Absolute path and filename to the assembly. public static string GetOriginalLocation(this Assembly thisAssembly) { return new Uri(thisAssembly.Location).LocalPath; } /// /// Version info of the executable which runs this code. /// public static FileVersionInfo ExecutableVersion => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.GetOriginalLocation()); /// /// Installs an assembly resolver. Mostly used to get shared assemblies that we don't want copied around to various output locations as happens when "Copy Local" is set to true /// for an assembly reference (which is the default). /// public static void InstallAssemblyResolver(string pathToBinariesDotNET) { AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { // Name is fully qualified assembly definition - e.g. "p4dn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ff968dc1933aba6f" string assemblyName = args.Name!.Split(',')[0]; return ( from knownAssemblyName in new[] { "SwarmAgent.exe", "../ThirdParty/Ionic/Ionic.Zip.Reduced.dll", "../ThirdParty/Newtonsoft/NewtonSoft.Json.dll" } where assemblyName.Equals(Path.GetFileNameWithoutExtension(knownAssemblyName), StringComparison.OrdinalIgnoreCase) let resolvedAssemblyFilename = Path.Combine(pathToBinariesDotNET, knownAssemblyName) // check if the file exists first. If we just try to load it, we correctly throw an exception, but it's a generic // FileNotFoundException, which is not informative. Better to return null. select File.Exists(resolvedAssemblyFilename) ? Assembly.LoadFile(resolvedAssemblyFilename) : null ).FirstOrDefault(); }; } /// /// Installs an assembly resolver, which will load *any* assembly which exists recursively within the supplied folder. /// /// The directory to enumerate. public static void InstallRecursiveAssemblyResolver(string rootDirectory) { RefreshAssemblyCache(rootDirectory); AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { // Name is fully qualified assembly definition - e.g. "p4dn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ff968dc1933aba6f" string assemblyName = args.Name!.Split(',')[0]; // The assembly wasn't found by other resolvers, though may have been compiled or copied as a dependency RefreshAssemblyCache(rootDirectory, String.Format("{0}.dll", assemblyName)); if (s_assemblyLocationCache.TryGetValue(assemblyName, out string? assemblyLocation)) { return Assembly.LoadFile(assemblyLocation); } return null; }; } private static void RefreshAssemblyCache(string rootDirectory, string pattern = "*.dll") { if (Directory.Exists(rootDirectory)) { // Initialize our cache of assemblies by enumerating all files in the given folder. foreach (string discoveredAssembly in Directory.EnumerateFiles(rootDirectory, pattern, SearchOption.AllDirectories)) { AddFileToAssemblyCache(discoveredAssembly); } } } /// /// Adds a file to the cache /// /// public static void AddFileToAssemblyCache(string assemblyPath) { // Ignore any reference assemblies string? directory = Path.GetFileName(Path.GetDirectoryName(assemblyPath)); if (!String.IsNullOrEmpty(directory) && (directory == "ref" | directory == "refint")) { return; } string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); DateTime assemblyLastWriteTime = File.GetLastWriteTimeUtc(assemblyPath); lock(s_assemblyLocationCache) { if (s_assemblyLocationCache.ContainsKey(assemblyName)) { // We already have this assembly in our cache. Only replace it if the discovered file is newer (to avoid stale assemblies breaking stuff). if (assemblyLastWriteTime > s_assemblyWriteTimes[assemblyName]) { s_assemblyLocationCache[assemblyName] = assemblyPath; s_assemblyWriteTimes[assemblyName] = assemblyLastWriteTime; } } else { // This is the first copy of this assembly ... add it to our cache. s_assemblyLocationCache.Add(assemblyName, assemblyPath); s_assemblyWriteTimes.Add(assemblyName, assemblyLastWriteTime); } } if (!s_addedToAssemblyResolver) { AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { // Name is fully qualified assembly definition - e.g. "p4dn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ff968dc1933aba6f" string assemblyName = args.Name!.Split(',')[0]; lock (s_assemblyLocationCache) { if (s_assemblyLocationCache.TryGetValue(assemblyName, out string? assemblyLocation)) { // We have this assembly in our folder. if (File.Exists(assemblyLocation)) { // The assembly still exists, so load it. return Assembly.LoadFile(assemblyLocation); } else { // The assembly no longer exists on disk, so remove it from our cache. s_assemblyLocationCache.Remove(assemblyName); } } } return null; }; s_addedToAssemblyResolver = true; } } // Map of assembly name to path on disk private static readonly Dictionary s_assemblyLocationCache = new Dictionary(StringComparer.OrdinalIgnoreCase); // Track last modified date of each assembly, so we can ensure we always reference the latest one in the case of stale assemblies on disk. private static readonly Dictionary s_assemblyWriteTimes = new Dictionary(StringComparer.OrdinalIgnoreCase); // Flag used to make sure we don't redundantly add resolvers private static bool s_addedToAssemblyResolver = false; } }