Browse Source

Implement type loader cache and simplify metadata types

ghorsington 4 years ago
parent
commit
d2a925e61b

+ 7 - 12
BepInEx.Preloader/Patching/AssemblyPatcher.cs

@@ -67,8 +67,7 @@ namespace BepInEx.Preloader.Patching
 
 			return new PatcherPlugin
 			{
-				Type = type,
-				Name = type.FullName
+				TypeName = type.FullName
 			};
 		}
 
@@ -88,16 +87,16 @@ namespace BepInEx.Preloader.Patching
 
 			foreach (var keyValuePair in patchers)
 			{
-				var assembly = keyValuePair.Key;
+				var assemblyPath = keyValuePair.Key;
 				var patcherCollection = keyValuePair.Value;
 
-				var ass = Assembly.LoadFile(assembly.MainModule.FileName);
+				var ass = Assembly.LoadFile(assemblyPath);
 
 				foreach (var patcherPlugin in patcherCollection)
 				{
 					try
 					{
-						var type = ass.GetType(patcherPlugin.Type.FullName);
+						var type = ass.GetType(patcherPlugin.TypeName);
 
 						var methods = type.GetMethods(ALL);
 
@@ -128,11 +127,10 @@ namespace BepInEx.Preloader.Patching
 						};
 
 						sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", patcherPlugin);
-						patcherPlugin.Type = null;
 					}
 					catch (Exception e)
 					{
-						Logger.LogError($"Failed to load patcher [{patcherPlugin.Type.FullName}]: {e.Message}");
+						Logger.LogError($"Failed to load patcher [{patcherPlugin.TypeName}]: {e.Message}");
 						if (e is ReflectionTypeLoadException re)
 							Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
 						else
@@ -141,14 +139,11 @@ namespace BepInEx.Preloader.Patching
 				}
 
 				Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
-					$"Loaded {patcherCollection.Count} patcher methods from {assembly.Name.Name}");
+					$"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}");
 			}
 
 			foreach (KeyValuePair<string, PatcherPlugin> patcher in sortedPatchers)
 				AddPatcher(patcher.Value);
-
-			foreach (var assemblyDefinition in patchers.Keys)
-				assemblyDefinition.Dispose();
 		}
 
 		private static void InitializePatchers()
@@ -213,7 +208,7 @@ namespace BepInEx.Preloader.Patching
 				foreach (string targetDll in assemblyPatcher.TargetDLLs())
 					if (assemblies.TryGetValue(targetDll, out var assembly))
 					{
-						Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.Name}]");
+						Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]");
 
 						assemblyPatcher.Patcher?.Invoke(ref assembly);
 						assemblies[targetDll] = assembly;

+ 15 - 6
BepInEx.Preloader/Patching/PatcherPlugin.cs

@@ -1,16 +1,15 @@
 using System;
 using System.Collections.Generic;
-using Mono.Cecil;
+using System.IO;
+using BepInEx.Bootstrap;
 
 namespace BepInEx.Preloader.Patching
 {
 	/// <summary>
 	///     A single assembly patcher.
 	/// </summary>
-	internal class PatcherPlugin
+	internal class PatcherPlugin : ICacheable
 	{
-		public TypeDefinition Type { get; set; }
-
 		/// <summary>
 		///     Target assemblies to patch.
 		/// </summary>
@@ -32,8 +31,18 @@ namespace BepInEx.Preloader.Patching
 		public AssemblyPatcherDelegate Patcher { get; set; } = null;
 
 		/// <summary>
-		///     Name of the patcher.
+		///     Type name of the patcher.
 		/// </summary>
-		public string Name { get; set; } = string.Empty;
+		public string TypeName { get; set; } = string.Empty;
+
+		public void Save(BinaryWriter bw)
+		{
+			bw.Write(TypeName);
+		}
+
+		public void Load(BinaryReader br)
+		{
+			TypeName = br.ReadString();
+		}
 	}
 }

+ 1 - 1
BepInEx.Preloader/Preloader.cs

@@ -111,7 +111,7 @@ namespace BepInEx.Preloader
 				{
 					TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
 					Patcher = PatchEntrypoint,
-					Name = "BepInEx.Chainloader"
+					TypeName = "BepInEx.Chainloader"
 				});
 
 				AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

+ 9 - 13
BepInEx/Bootstrap/Chainloader.cs

@@ -135,8 +135,7 @@ namespace BepInEx.Bootstrap
 				Metadata = metadata,
 				Processes = filters,
 				Dependencies = dependencies,
-				CecilType = type,
-				Location = type.Module.FileName
+				TypeName = type.FullName
 			};
 		}
 
@@ -181,9 +180,12 @@ namespace BepInEx.Bootstrap
 
 				UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
 
-				var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins);
+				var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
+				foreach (var keyValuePair in pluginsToLoad)
+					foreach (var pluginInfo in keyValuePair.Value)
+						pluginInfo.Location = keyValuePair.Key;
 				var pluginInfos = pluginsToLoad.SelectMany(p => p.Value).ToList();
-				var loadedAssemblies = new Dictionary<AssemblyDefinition, Assembly>();
+				var loadedAssemblies = new Dictionary<string, Assembly>();
 
 				Logger.LogInfo($"{pluginInfos.Count} plugins to load");
 
@@ -266,12 +268,11 @@ namespace BepInEx.Bootstrap
 					{
 						Logger.LogInfo($"Loading [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
 
-						if (!loadedAssemblies.TryGetValue(pluginInfo.CecilType.Module.Assembly, out var ass))
-							loadedAssemblies[pluginInfo.CecilType.Module.Assembly] = ass = Assembly.LoadFile(pluginInfo.Location);
+						if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
+							loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
 
 						PluginInfos[pluginGUID] = pluginInfo;
-						pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.CecilType.FullName));
-						pluginInfo.CecilType = null;
+						pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));
 
 						Plugins.Add(pluginInfo.Instance);
 					}
@@ -287,11 +288,6 @@ namespace BepInEx.Bootstrap
 							Logger.LogDebug(ex);
 					}
 				}
-
-				foreach (var selectedTypesInfo in pluginsToLoad)
-				{
-					selectedTypesInfo.Key.Dispose();
-				}
 			}
 			catch (Exception ex)
 			{

+ 155 - 14
BepInEx/Bootstrap/TypeLoader.cs

@@ -10,14 +10,48 @@ using Mono.Cecil;
 namespace BepInEx.Bootstrap
 {
 	/// <summary>
-	/// Provides methods for loading specified types from an assembly.
+	/// A cacheable metadata item. Can be used with <see cref="TypeLoader.LoadAssemblyCache{T}"/> and <see cref="TypeLoader.SaveAssemblyCache{T}"/> to cache plugin metadata.
 	/// </summary>
-	public static class TypeLoader
+	public interface ICacheable
 	{
-		private static DefaultAssemblyResolver resolver;
-		private static ReaderParameters readerParameters;
+		/// <summary>
+		/// Serialize the object into a binary format.
+		/// </summary>
+		/// <param name="bw"></param>
+		void Save(BinaryWriter bw);
 
-		public static event AssemblyResolveEventHandler AssemblyResolve;
+		/// <summary>
+		/// Loads the object from binary format.
+		/// </summary>
+		/// <param name="br"></param>
+		void Load(BinaryReader br);
+	}
+
+	/// <summary>
+	/// A cached assembly.
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class CachedAssembly<T> where T : ICacheable
+	{
+		/// <summary>
+		/// List of cached items inside the assembly.
+		/// </summary>
+		public List<T> CacheItems { get; set; }
+
+
+		/// <summary>
+		/// Timestamp of the assembly. Used to check the age of the cache.
+		/// </summary>
+		public long Timestamp { get; set; }
+	}
+
+	/// <summary>
+	///     Provides methods for loading specified types from an assembly.
+	/// </summary>
+	public static class TypeLoader
+	{
+		private static readonly DefaultAssemblyResolver resolver;
+		private static readonly ReaderParameters readerParameters;
 
 		static TypeLoader()
 		{
@@ -28,7 +62,7 @@ namespace BepInEx.Bootstrap
 			{
 				var name = new AssemblyName(reference.FullName);
 
-				if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out AssemblyDefinition assembly) ||
+				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;
@@ -37,20 +71,38 @@ namespace BepInEx.Bootstrap
 			};
 		}
 
+		public static event AssemblyResolveEventHandler AssemblyResolve;
+
 		/// <summary>
-		/// Loads a list of types from a directory containing assemblies, that derive from a base type.
+		///     Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata.
 		/// </summary>
 		/// <typeparam name="T">The specific base type to search for.</typeparam>
 		/// <param name="directory">The directory to search for assemblies.</param>
-		/// <returns>Returns a list of found derivative types.</returns>
-		public static Dictionary<AssemblyDefinition, List<T>> FindPluginTypes<T>(string directory, Func<TypeDefinition, T> typeSelector, Func<AssemblyDefinition, bool> assemblyFilter = null) where T : class
+		/// <param name="typeSelector">A function to check if a type should be selected and to build the type metadata.</param>
+		/// <param name="assemblyFilter">A filter function to quickly determine if the assembly can be loaded.</param>
+		/// <param name="cacheName">The name of the cache to get cached types from.</param>
+		/// <returns>A list of all loadable type metadatas indexed by the full path to the assembly that contains the types.</returns>
+		public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory, Func<TypeDefinition, T> typeSelector, Func<AssemblyDefinition, bool> assemblyFilter = null, string cacheName = null) where T : ICacheable, new()
 		{
-			var result = new Dictionary<AssemblyDefinition, List<T>>();
+			var result = new Dictionary<string, List<T>>();
+			Dictionary<string, CachedAssembly<T>> cache = null;
+
+			if (cacheName != null)
+				cache = LoadAssemblyCache<T>(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)
@@ -67,21 +119,110 @@ namespace BepInEx.Bootstrap
 						continue;
 					}
 
-					result[ass] = matches;
+					result[dll] = matches;
+					ass.Dispose();
 				}
 				catch (Exception e)
 				{
 					Logger.LogError(e.ToString());
 				}
+
+			if (cacheName != null)
+				SaveAssemblyCache(cacheName, result);
+
+			return result;
+		}
+
+		/// <summary>
+		///     Loads an index of type metadatas from a cache.
+		/// </summary>
+		/// <param name="cacheName">Name of the cache</param>
+		/// <typeparam name="T">Cacheable item</typeparam>
+		/// <returns>Cached type metadatas indexed by the path of the assembly that defines the type</returns>
+		public static Dictionary<string, CachedAssembly<T>> LoadAssemblyCache<T>(string cacheName) where T : ICacheable, new()
+		{
+			var result = new Dictionary<string, CachedAssembly<T>>();
+			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<T>();
+
+						for (var j = 0; j < itemsCount; j++)
+						{
+							var entry = new T();
+							entry.Load(br);
+							items.Add(entry);
+						}
+
+						result[entryIdentifier] = new CachedAssembly<T> { Timestamp = entryDate, CacheItems = items };
+					}
+				}
+			}
+			catch (Exception e)
+			{
+				Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}.");
 			}
 
 			return result;
 		}
 
+		/// <summary>
+		///     Saves indexed type metadata into a cache.
+		/// </summary>
+		/// <param name="cacheName">Name of the cache</param>
+		/// <param name="entries">List of plugin metadatas indexed by the path to the assembly that contains the types</param>
+		/// <typeparam name="T">Cacheable item</typeparam>
+		public static void SaveAssemblyCache<T>(string cacheName, Dictionary<string, List<T>> entries) where T : ICacheable
+		{
+			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}.");
+			}
+		}
+
+		/// <summary>
+		///     Converts TypeLoadException to a readable string.
+		/// </summary>
+		/// <param name="ex">TypeLoadException</param>
+		/// <returns>Readable representation of the exception</returns>
 		public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex)
 		{
-			StringBuilder sb = new StringBuilder();
-			foreach (Exception exSub in ex.LoaderExceptions)
+			var sb = new StringBuilder();
+			foreach (var exSub in ex.LoaderExceptions)
 			{
 				sb.AppendLine(exSub.Message);
 				if (exSub is FileNotFoundException exFileNotFound)

+ 46 - 3
BepInEx/Contract/PluginInfo.cs

@@ -1,9 +1,11 @@
 using System.Collections.Generic;
-using Mono.Cecil;
+using System.IO;
+using System.Linq;
+using BepInEx.Bootstrap;
 
 namespace BepInEx.Contract
 {
-	public class PluginInfo
+	public class PluginInfo : ICacheable
 	{
 		public BepInPlugin Metadata { get; internal set; }
 
@@ -15,6 +17,47 @@ namespace BepInEx.Contract
 
 		public BaseUnityPlugin Instance { get; internal set; }
 
-		internal TypeDefinition CecilType { get; set; }
+		internal string TypeName { get; set; }
+
+		public void Save(BinaryWriter bw)
+		{
+			bw.Write(TypeName);
+
+			bw.Write(Metadata.GUID);
+			bw.Write(Metadata.Name);
+			bw.Write(Metadata.Version.ToString());
+
+			var processList = Processes.ToList();
+			bw.Write(processList.Count);
+			foreach (var bepInProcess in processList)
+				bw.Write(bepInProcess.ProcessName);
+
+			var depList = Dependencies.ToList();
+			bw.Write(depList.Count);
+			foreach (var bepInDependency in depList)
+			{
+				bw.Write(bepInDependency.DependencyGUID);
+				bw.Write((int)bepInDependency.Flags);
+			}
+		}
+
+		public void Load(BinaryReader br)
+		{
+			TypeName = br.ReadString();
+
+			Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString());
+
+			var processListCount = br.ReadInt32();
+			var processList = new List<BepInProcess>(processListCount);
+			for (int i = 0; i < processListCount; i++)
+				processList.Add(new BepInProcess(br.ReadString()));
+			Processes = processList;
+
+			var depCount = br.ReadInt32();
+			var depList = new List<BepInDependency>(depCount);
+			for (int i = 0; i < depCount; i++)
+				depList.Add(new BepInDependency(br.ReadString(), (BepInDependency.DependencyFlags) br.ReadInt32()));
+			Dependencies = depList;
+		}
 	}
 }

+ 6 - 0
BepInEx/Paths.cs

@@ -21,6 +21,7 @@ namespace BepInEx
 			PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers");
 			BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core");
 			BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.dll");
+			CachePath = Path.Combine(BepInExRootPath, "cache");
 		}
 
 		internal static void SetManagedPath(string managedPath)
@@ -76,6 +77,11 @@ namespace BepInEx
 		public static string BepInExConfigPath { get; private set; }
 
 		/// <summary>
+        ///		The path to temporary cache files.
+        /// </summary>
+		public static string CachePath { get; private set; }
+
+		/// <summary>
 		///     The path to the patcher plugin folder which resides in the BepInEx folder.
 		/// </summary>
 		public static string PatcherPluginPath { get; private set; }