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
}
}