using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using BepInEx.Configuration; using BepInEx.Logging; using Mono.Cecil; namespace BepInEx.Bootstrap { /// /// A cacheable metadata item. Can be used with and to cache plugin metadata. /// public interface ICacheable { /// /// Serialize the object into a binary format. /// /// void Save(BinaryWriter bw); /// /// Loads the object from binary format. /// /// void Load(BinaryReader br); } /// /// A cached assembly. /// /// public class CachedAssembly where T : ICacheable { /// /// List of cached items inside the assembly. /// public List CacheItems { get; set; } /// /// Timestamp of the assembly. Used to check the age of the cache. /// public long Timestamp { get; set; } } /// /// Provides methods for loading specified types from an assembly. /// public static class TypeLoader { private static readonly DefaultAssemblyResolver resolver; private static readonly ReaderParameters readerParameters; static TypeLoader() { resolver = new DefaultAssemblyResolver(); readerParameters = new ReaderParameters { AssemblyResolver = resolver }; resolver.ResolveFailure += (sender, reference) => { var name = new AssemblyName(reference.FullName); if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out var assembly) || Utility.TryResolveDllAssembly(name, Paths.PluginPath, readerParameters, out assembly) || Utility.TryResolveDllAssembly(name, Paths.ManagedPath, readerParameters, out assembly)) return assembly; return AssemblyResolve?.Invoke(sender, reference); }; } public static event AssemblyResolveEventHandler AssemblyResolve; /// /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata. /// /// The specific base type to search for. /// The directory to search for assemblies. /// A function to check if a type should be selected and to build the type metadata. /// A filter function to quickly determine if the assembly can be loaded. /// The name of the cache to get cached types from. /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the selector. public static Dictionary> FindPluginTypes(string directory, Func typeSelector, Func assemblyFilter = null, string cacheName = null) where T : ICacheable, new() { var result = new Dictionary>(); Dictionary> cache = null; if (cacheName != null) cache = LoadAssemblyCache(cacheName); foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) try { if (cache != null && cache.TryGetValue(dll, out var cacheEntry)) { long lastWrite = File.GetLastWriteTimeUtc(dll).Ticks; if (lastWrite == cacheEntry.Timestamp) { result[dll] = cacheEntry.CacheItems; continue; } } var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters); if (!assemblyFilter?.Invoke(ass) ?? false) { result[dll] = new List(); ass.Dispose(); continue; } var matches = ass.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList(); result[dll] = matches; ass.Dispose(); } catch (Exception e) { Logger.LogError(e.ToString()); } if (cacheName != null) SaveAssemblyCache(cacheName, result); return result; } /// /// Loads an index of type metadatas from a cache. /// /// Name of the cache /// Cacheable item /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined, return null. public static Dictionary> LoadAssemblyCache(string cacheName) where T : ICacheable, new() { if (!EnableAssemblyCache.Value) return null; var result = new Dictionary>(); try { string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); if (!File.Exists(path)) return null; using (var br = new BinaryReader(File.OpenRead(path))) { int entriesCount = br.ReadInt32(); for (var i = 0; i < entriesCount; i++) { string entryIdentifier = br.ReadString(); long entryDate = br.ReadInt64(); int itemsCount = br.ReadInt32(); var items = new List(); for (var j = 0; j < itemsCount; j++) { var entry = new T(); entry.Load(br); items.Add(entry); } result[entryIdentifier] = new CachedAssembly { Timestamp = entryDate, CacheItems = items }; } } } catch (Exception e) { Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}."); } return result; } /// /// Saves indexed type metadata into a cache. /// /// Name of the cache /// List of plugin metadatas indexed by the path to the assembly that contains the types /// Cacheable item public static void SaveAssemblyCache(string cacheName, Dictionary> entries) where T : ICacheable { if (!EnableAssemblyCache.Value) return; try { if (!Directory.Exists(Paths.CachePath)) Directory.CreateDirectory(Paths.CachePath); string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); using (var bw = new BinaryWriter(File.OpenWrite(path))) { bw.Write(entries.Count); foreach (var kv in entries) { bw.Write(kv.Key); bw.Write(File.GetLastWriteTimeUtc(kv.Key).Ticks); bw.Write(kv.Value.Count); foreach (var item in kv.Value) item.Save(bw); } } } catch (Exception e) { Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}."); } } /// /// Converts TypeLoadException to a readable string. /// /// TypeLoadException /// Readable representation of the exception public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex) { var sb = new StringBuilder(); foreach (var exSub in ex.LoaderExceptions) { sb.AppendLine(exSub.Message); if (exSub is FileNotFoundException exFileNotFound) { if (!string.IsNullOrEmpty(exFileNotFound.FusionLog)) { sb.AppendLine("Fusion Log:"); sb.AppendLine(exFileNotFound.FusionLog); } } else if (exSub is FileLoadException exLoad) { if (!string.IsNullOrEmpty(exLoad.FusionLog)) { sb.AppendLine("Fusion Log:"); sb.AppendLine(exLoad.FusionLog); } } sb.AppendLine(); } return sb.ToString(); } #region Config private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind( "Caching", "EnableAssemblyCache", true, "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers."); #endregion } }