From 45fae7daee2334b9fa3b81737d9ede3cc4440c69 Mon Sep 17 00:00:00 2001 From: Bepis <36346617+bbepis@users.noreply.github.com> Date: Thu, 7 Nov 2019 23:33:10 +1100 Subject: [PATCH] Base implementation for v2 patcher plugin contract --- BepInEx.Preloader/BepInEx.Preloader.csproj | 4 +- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 228 +++++++++++++++--- BepInEx.Preloader/Patching/Attributes.cs | 43 ++++ BepInEx.Preloader/Patching/BasePatcher.cs | 33 +++ BepInEx.Preloader/Patching/PatchContext.cs | 85 +++++++ BepInEx.Preloader/Patching/PatcherPlugin.cs | 48 ---- BepInEx.Preloader/Preloader.cs | 4 +- BepInEx/Bootstrap/TypeLoader.cs | 47 +++- 8 files changed, 400 insertions(+), 92 deletions(-) create mode 100644 BepInEx.Preloader/Patching/Attributes.cs create mode 100644 BepInEx.Preloader/Patching/BasePatcher.cs create mode 100644 BepInEx.Preloader/Patching/PatchContext.cs delete mode 100644 BepInEx.Preloader/Patching/PatcherPlugin.cs diff --git a/BepInEx.Preloader/BepInEx.Preloader.csproj b/BepInEx.Preloader/BepInEx.Preloader.csproj index 0f6996d..5c7cee9 100644 --- a/BepInEx.Preloader/BepInEx.Preloader.csproj +++ b/BepInEx.Preloader/BepInEx.Preloader.csproj @@ -48,8 +48,9 @@ + - + @@ -73,5 +74,6 @@ + \ No newline at end of file diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 8935781..1450b71 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; @@ -26,22 +27,14 @@ namespace BepInEx.Preloader.Patching { private const BindingFlags ALL = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.IgnoreCase; - public static List PatcherPlugins { get; } = new List(); + public static List LegacyPatcherPlugins { get; } = new List(); + public static List PatcherPlugins { get; } = new List(); private static readonly string DumpedAssembliesPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies"); - /// - /// Adds a single assembly patcher to the pool of applicable patches. - /// - /// Patcher to apply. - public static void AddPatcher(PatcherPlugin patcher) - { - PatcherPlugins.Add(patcher); - } - private static T CreateDelegate(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null; - private static PatcherPlugin ToPatcherPlugin(TypeDefinition type) + private static LegacyPatcherPlugin ToLegacyPatcherPlugin(TypeDefinition type) { if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; @@ -65,7 +58,30 @@ namespace BepInEx.Preloader.Patching if (patch == null) return null; - return new PatcherPlugin + return new LegacyPatcherPlugin + { + TypeName = type.FullName + }; + } + + private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$"); + private static PatcherDefinition ToPatcherPlugin(TypeDefinition type) + { + if (type.IsInterface || type.IsAbstract) + return null; + + try + { + if (!type.IsSubtypeOf(typeof(BasePatcher))) + return null; + } + catch (AssemblyResolutionException) + { + // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin. + return null; + } + + return new PatcherDefinition { TypeName = type.FullName }; @@ -81,25 +97,24 @@ namespace BepInEx.Preloader.Patching if (!Directory.Exists(directory)) return; - var sortedPatchers = new SortedDictionary(); + var legacyPatchers = TypeLoader.FindPluginTypes(directory, ToLegacyPatcherPlugin); + var patchers = TypeLoader.FindPluginTypesCacheless(directory, ToPatcherPlugin); - var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin); - - foreach (var keyValuePair in patchers) + foreach (var keyValuePair in legacyPatchers) { var assemblyPath = keyValuePair.Key; var patcherCollection = keyValuePair.Value; - if(patcherCollection.Count == 0) + if (patcherCollection.Count == 0) continue; - var ass = Assembly.LoadFile(assemblyPath); + var assembly = Assembly.LoadFile(assemblyPath); foreach (var patcherPlugin in patcherCollection) { try { - var type = ass.GetType(patcherPlugin.TypeName); + var type = assembly.GetType(patcherPlugin.TypeName); var methods = type.GetMethods(ALL); @@ -129,7 +144,7 @@ namespace BepInEx.Preloader.Patching pAss = (AssemblyDefinition)args[0]; }; - sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", patcherPlugin); + LegacyPatcherPlugins.Add(patcherPlugin); } catch (Exception e) { @@ -142,23 +157,111 @@ namespace BepInEx.Preloader.Patching } Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, - $"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}"); + $"Loaded {patcherCollection.Count} legacy patcher methods from {assembly.GetName().FullName}"); } - foreach (KeyValuePair patcher in sortedPatchers) - AddPatcher(patcher.Value); + foreach (var keyValuePair in patchers) + { + var assemblyPath = keyValuePair.Key; + var patcherCollection = keyValuePair.Value; + + if (patcherCollection.Count == 0) + continue; + + var assembly = Assembly.LoadFile(assemblyPath); + + foreach (var patcherPlugin in patcherCollection) + { + try + { + var type = assembly.GetType(patcherPlugin.TypeName); + + patcherPlugin.PatcherInfo = (PatcherInfo)type.GetCustomAttributes(typeof(PatcherInfo), false).FirstOrDefault(); + + if (patcherPlugin.PatcherInfo == null) + { + Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); + continue; + } + + if (string.IsNullOrEmpty(patcherPlugin.PatcherInfo.GUID) || !allowedGuidRegex.IsMatch(patcherPlugin.PatcherInfo.GUID)) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{patcherPlugin.PatcherInfo.GUID}] is of an illegal format."); + continue; + } + + if (patcherPlugin.PatcherInfo.Version == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid."); + continue; + } + + if (patcherPlugin.PatcherInfo.Name == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null."); + continue; + } + + var methods = type.GetMethods(ALL); + + foreach (var method in methods) + { + var parameters = method.GetParameters(); + + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(AssemblyDefinition)) + { + var targetAssemblyAttributes = method.GetCustomAttributes(typeof(TargetAssemblyAttribute), false); + + foreach (var attribute in targetAssemblyAttributes) + { + patcherPlugin.AssemblyDefinitionPatchers[(TargetAssemblyAttribute)attribute] = method; + } + } + else if (parameters.Length == 1 && parameters[0].ParameterType == typeof(TypeDefinition)) + { + var targetClassAttributes = method.GetCustomAttributes(typeof(TargetClassAttribute), false); + + foreach (var attribute in targetClassAttributes) + { + patcherPlugin.TypeDefinitionPatchers[(TargetClassAttribute)attribute] = method; + } + } + } + + patcherPlugin.Instance = (BasePatcher)Activator.CreateInstance(type); + + Logger.LogInfo($"Loaded patcher [{patcherPlugin.PatcherInfo.Name} {patcherPlugin.PatcherInfo.Version}]"); + + PatcherPlugins.Add(patcherPlugin); + } + catch (Exception e) + { + Logger.LogError($"Failed to load patcher [{patcherPlugin.TypeName}]: {e.Message}"); + if (e is ReflectionTypeLoadException re) + Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re)); + else + Logger.LogDebug(e.ToString()); + } + } + } } - private static void InitializePatchers() + private static void InitializePatchers(PatchContext context) { - foreach (var assemblyPatcher in PatcherPlugins) + foreach (var assemblyPatcher in LegacyPatcherPlugins) assemblyPatcher.Initializer?.Invoke(); + + foreach (var patcher in PatcherPlugins) + patcher.Instance.Setup(context); } private static void FinalizePatching() { - foreach (var assemblyPatcher in PatcherPlugins) + foreach (var assemblyPatcher in LegacyPatcherPlugins) assemblyPatcher.Finalizer?.Invoke(); + + foreach (var patcher in PatcherPlugins) + patcher.Instance.Finalize(); } /// @@ -166,7 +269,7 @@ namespace BepInEx.Preloader.Patching /// public static void DisposePatchers() { - PatcherPlugins.Clear(); + LegacyPatcherPlugins.Clear(); } /// @@ -176,7 +279,8 @@ namespace BepInEx.Preloader.Patching public static void PatchAndLoad(string directory) { // First, load patchable assemblies into Cecil - var assemblies = new Dictionary(); + var assemblies = new List(); + var filenameDictionary = new Dictionary(); foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll")) { @@ -197,30 +301,82 @@ namespace BepInEx.Preloader.Patching Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping..."); continue; } - - assemblies.Add(Path.GetFileName(assemblyPath), assembly); + + assemblies.Add(assembly); + filenameDictionary[Path.GetFileName(assemblyPath)] = assembly; UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath)); } + // Create the patch context + PatchContext context = new PatchContext + { + LoadedPatchers = PatcherPlugins.AsReadOnly(), + AvailableAssemblies = assemblies, + AssemblyLocations = filenameDictionary + }; + // Next, initialize all the patchers - InitializePatchers(); + InitializePatchers(context); // Then, perform the actual patching + + // Legacy patchers var patchedAssemblies = new HashSet(); - foreach (var assemblyPatcher in PatcherPlugins) + foreach (var assemblyPatcher in LegacyPatcherPlugins) foreach (string targetDll in assemblyPatcher.TargetDLLs()) - if (assemblies.TryGetValue(targetDll, out var assembly)) + if (filenameDictionary.TryGetValue(targetDll, out var assembly)) { if (AppDomain.CurrentDomain.GetAssemblies().Any(x => x.GetName().Name == assembly.Name.Name)) Logger.LogWarning($"Trying to patch an already loaded assembly [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]"); Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]"); + var oldAssembly = assembly; + assemblyPatcher.Patcher?.Invoke(ref assembly); - assemblies[targetDll] = assembly; + + if (!ReferenceEquals(oldAssembly, assembly)) + { + assemblies.Remove(oldAssembly); + assemblies.Add(assembly); + } + + filenameDictionary[targetDll] = assembly; patchedAssemblies.Add(targetDll); } + // V2 patchers + + foreach (var patcherDefinition in PatcherPlugins) + { + patcherDefinition.Instance.PatchAll(assemblies); + } + + foreach (var patcherDefinition in PatcherPlugins) + { + if (patcherDefinition.AssemblyDefinitionPatchers != null) + foreach (var assemblyPatchDefinition in patcherDefinition.AssemblyDefinitionPatchers) + { + var assemblyDefinition = assemblies.FirstOrDefault(x => x.Name.Name == assemblyPatchDefinition.Key.AssemblyName); + patchedAssemblies.Add(filenameDictionary.First(x => x.Value == assemblyDefinition).Key); + + assemblyPatchDefinition.Value.Invoke(patcherDefinition.Instance, new object[] { assemblyDefinition }); + } + } + + foreach (var patcherDefinition in PatcherPlugins) + { + if (patcherDefinition.TypeDefinitionPatchers != null) + foreach (var typePatchDefinition in patcherDefinition.TypeDefinitionPatchers) + { + var assemblyDefinition = assemblies.FirstOrDefault(x => x.Name.Name == typePatchDefinition.Key.AssemblyName); + patchedAssemblies.Add(filenameDictionary.First(x => x.Value == assemblyDefinition).Key); + + var typeDefinition = assemblyDefinition?.MainModule.Types.FirstOrDefault(x => x.FullName == typePatchDefinition.Key.ClassName); + + typePatchDefinition.Value.Invoke(patcherDefinition.Instance, new object[] { typeDefinition }); + } + } // Finally, load patched assemblies into memory if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) @@ -228,7 +384,7 @@ namespace BepInEx.Preloader.Patching if (!Directory.Exists(DumpedAssembliesPath)) Directory.CreateDirectory(DumpedAssembliesPath); - foreach (KeyValuePair kv in assemblies) + foreach (KeyValuePair kv in filenameDictionary) { string filename = kv.Key; var assembly = kv.Value; @@ -246,7 +402,7 @@ namespace BepInEx.Preloader.Patching Debugger.Break(); } - foreach (var kv in assemblies) + foreach (var kv in filenameDictionary) { string filename = kv.Key; var assembly = kv.Value; diff --git a/BepInEx.Preloader/Patching/Attributes.cs b/BepInEx.Preloader/Patching/Attributes.cs new file mode 100644 index 0000000..904aa1f --- /dev/null +++ b/BepInEx.Preloader/Patching/Attributes.cs @@ -0,0 +1,43 @@ +using System; + +namespace BepInEx.Preloader +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class TargetAssemblyAttribute : Attribute + { + public string AssemblyName { get; } + + public TargetAssemblyAttribute(string assemblyName) + { + AssemblyName = assemblyName; + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class TargetClassAttribute : Attribute + { + public string AssemblyName { get; } + public string ClassName { get; } + + public TargetClassAttribute(string assemblyName, string className) + { + AssemblyName = assemblyName; + ClassName = className; + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class PatcherInfo : Attribute + { + public string GUID { get; } + public string Name { get; } + public Version Version { get; } + + public PatcherInfo(string guid, string name, string version) + { + GUID = guid; + Name = name; + Version = new Version(version); + } + } +} \ No newline at end of file diff --git a/BepInEx.Preloader/Patching/BasePatcher.cs b/BepInEx.Preloader/Patching/BasePatcher.cs new file mode 100644 index 0000000..a3dabe7 --- /dev/null +++ b/BepInEx.Preloader/Patching/BasePatcher.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using BepInEx.Preloader.Patching; +using Mono.Cecil; + +namespace BepInEx.Preloader +{ + public abstract class BasePatcher + { + private PatchContext _patchContext; + protected PatchContext PatchContext => _patchContext; + + public virtual void Setup(PatchContext patchContext) + { + _patchContext = patchContext; + } + + public virtual void PatchAll(IList assemblyDefinitions) + { + + } + + public virtual void Finalize() + { + + } + + [TargetAssembly(assemblyName: "Terraria")] + public void Patch(AssemblyDefinition assemblyDefinition) + { + + } + } +} diff --git a/BepInEx.Preloader/Patching/PatchContext.cs b/BepInEx.Preloader/Patching/PatchContext.cs new file mode 100644 index 0000000..223fd0e --- /dev/null +++ b/BepInEx.Preloader/Patching/PatchContext.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Reflection; +using BepInEx.Bootstrap; +using Mono.Cecil; + +namespace BepInEx.Preloader.Patching +{ + public class PatchContext + { + public ReadOnlyCollection LoadedPatchers { get; internal set; } + + /// + /// A read-only list of legacy patchers that are loaded. Will be removed in a future release. + /// + [Obsolete("Will be removed in a future release.", false)] + public IList LoadedLegacyPatchers { get; internal set; } + + public IList AvailableAssemblies { get; internal set; } + + public Dictionary AssemblyLocations { get; internal set; } + + internal PatchContext() { } + } + + /// + /// A preloader assembly patcher. + /// + public class PatcherDefinition + { + public PatcherInfo PatcherInfo { get; internal set; } + + public string TypeName { get; internal set; } + public BasePatcher Instance { get; internal set; } + + public Dictionary AssemblyDefinitionPatchers { get; internal set; } = new Dictionary(); + + public Dictionary TypeDefinitionPatchers { get; internal set; } = new Dictionary(); + + internal PatcherDefinition() { } + } + + /// + /// A single assembly patcher, based on the old reflection contract. + /// + internal class LegacyPatcherPlugin : ICacheable + { + /// + /// Target assemblies to patch. + /// + public Func> TargetDLLs { get; set; } = null; + + /// + /// Initializer method that is run before any patching occurs. + /// + public Action Initializer { get; set; } = null; + + /// + /// Finalizer method that is run after all patching is done. + /// + public Action Finalizer { get; set; } = null; + + /// + /// The main patcher method that is called on every DLL defined in . + /// + public AssemblyPatcherDelegate Patcher { get; set; } = null; + + /// + /// Type name of the patcher. + /// + public string TypeName { get; set; } = string.Empty; + + public void Save(BinaryWriter bw) + { + bw.Write(TypeName); + } + + public void Load(BinaryReader br) + { + TypeName = br.ReadString(); + } + } +} diff --git a/BepInEx.Preloader/Patching/PatcherPlugin.cs b/BepInEx.Preloader/Patching/PatcherPlugin.cs deleted file mode 100644 index 390cc88..0000000 --- a/BepInEx.Preloader/Patching/PatcherPlugin.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using BepInEx.Bootstrap; - -namespace BepInEx.Preloader.Patching -{ - /// - /// A single assembly patcher. - /// - internal class PatcherPlugin : ICacheable - { - /// - /// Target assemblies to patch. - /// - public Func> TargetDLLs { get; set; } = null; - - /// - /// Initializer method that is run before any patching occurs. - /// - public Action Initializer { get; set; } = null; - - /// - /// Finalizer method that is run after all patching is done. - /// - public Action Finalizer { get; set; } = null; - - /// - /// The main patcher method that is called on every DLL defined in . - /// - public AssemblyPatcherDelegate Patcher { get; set; } = null; - - /// - /// Type name of the patcher. - /// - public string TypeName { get; set; } = string.Empty; - - public void Save(BinaryWriter bw) - { - bw.Write(TypeName); - } - - public void Load(BinaryReader br) - { - TypeName = br.ReadString(); - } - } -} \ No newline at end of file diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index d6f6c69..9d4867e 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -79,7 +79,7 @@ namespace BepInEx.Preloader Logger.LogMessage("Preloader started"); - AssemblyPatcher.AddPatcher(new PatcherPlugin + AssemblyPatcher.LegacyPatcherPlugins.Add(new LegacyPatcherPlugin { TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value }, Patcher = PatchEntrypoint, @@ -88,7 +88,7 @@ namespace BepInEx.Preloader AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); - Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded"); + Logger.LogInfo($"{AssemblyPatcher.LegacyPatcherPlugins.Count + AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded"); AssemblyPatcher.PatchAndLoad(Paths.ManagedPath); AssemblyPatcher.DisposePatchers(); diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index 9a4b865..d6f3dc0 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -104,18 +104,18 @@ namespace BepInEx.Bootstrap } } - var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters); + var assembly = AssemblyDefinition.ReadAssembly(dll, readerParameters); - if (!assemblyFilter?.Invoke(ass) ?? false) + if (!assemblyFilter?.Invoke(assembly) ?? false) { result[dll] = new List(); - ass.Dispose(); + assembly.Dispose(); continue; } - var matches = ass.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList(); + var matches = assembly.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList(); result[dll] = matches; - ass.Dispose(); + assembly.Dispose(); } catch (Exception e) { @@ -128,6 +128,43 @@ namespace BepInEx.Bootstrap return result; } + /// + /// 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> FindPluginTypesCacheless(string directory, Func typeSelector, Func assemblyFilter = null) + { + var result = new Dictionary>(); + + foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) + try + { + var assembly = AssemblyDefinition.ReadAssembly(dll, readerParameters); + + if (!assemblyFilter?.Invoke(assembly) ?? false) + { + result[dll] = new List(); + assembly.Dispose(); + continue; + } + + var matches = assembly.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList(); + result[dll] = matches; + assembly.Dispose(); + } + catch (Exception e) + { + Logger.LogError(e.ToString()); + } + + return result; + } + /// /// Loads an index of type metadatas from a cache. ///