From 59437a8138e9adb8b86f51bc3dc6e4d2ff22df5e Mon Sep 17 00:00:00 2001 From: Katy Coe Date: Sat, 19 Dec 2020 20:46:58 +0100 Subject: [PATCH] Plugins: Implement PluginManager --- Il2CppInspector.Common/Il2CppInspector.csproj | 4 +- .../Plugins/PluginManager.cs | 228 ++++++++++++++++++ 2 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 Il2CppInspector.Common/Plugins/PluginManager.cs diff --git a/Il2CppInspector.Common/Il2CppInspector.csproj b/Il2CppInspector.Common/Il2CppInspector.csproj index 4dc2004..56ff45f 100644 --- a/Il2CppInspector.Common/Il2CppInspector.csproj +++ b/Il2CppInspector.Common/Il2CppInspector.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + netcoreapp3.1 Il2CppInspector.Common Katy Coe 2020.2.1 @@ -38,11 +38,11 @@ + NU1605 - diff --git a/Il2CppInspector.Common/Plugins/PluginManager.cs b/Il2CppInspector.Common/Plugins/PluginManager.cs new file mode 100644 index 0000000..da728fb --- /dev/null +++ b/Il2CppInspector.Common/Plugins/PluginManager.cs @@ -0,0 +1,228 @@ +/* + Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using McMaster.NETCore.Plugins; + +// This is the ONLY line to update when the API version changes +using Il2CppInspector.PluginAPI.V100; + +namespace Il2CppInspector +{ + // Internal settings for a plugin + public class ManagedPlugin + { + // The plugin itself + public IPlugin Plugin { get; set; } + + // The plugin is enabled for execution + public bool Enabled { get; set; } + + // The plugin is valid and compatible with this version of the host + public bool Available { get; set; } + } + + // Event arguments for error handler + public class PluginErrorEventArgs : EventArgs + { + // The plugin that the event originated from + public IPlugin Plugin { get; set; } + + // The exception thrown + public Exception Exception { get; set; } + + // The name of the operation that was being performed + public string Operation { get; set; } + } + + // Event arguments for the status handler + public class PluginStatusEventArgs : EventArgs + { + // The plugin that the event originated from + public IPlugin Plugin { get; set; } + + // The status update text + public string Text { get; set; } + } + + // Singleton for managing external plugins + public partial class PluginManager + { + // All of the detected plugins, including invalid/incompatible/non-loaded plugins + public ObservableCollection ManagedPlugins { get; } = new ObservableCollection(); + + // All of the plugins that are loaded and available for use + public static IEnumerable AvailablePlugins => AsInstance.ManagedPlugins.Where(p => p.Available).Select(p => p.Plugin); + + // All of the plugins that are currently enabled and will be called into + public static IEnumerable EnabledPlugins => AsInstance.ManagedPlugins.Where(p => p.Enabled).Select(p => p.Plugin); + + // The relative path from the executable that we'll search for plugins + private static string pluginFolder = Path.GetFullPath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + Path.DirectorySeparatorChar + "plugins"); + + // A placeholder plugin to be used when the real plugin cannot be loaded for some reason + private class InvalidPlugin : IPlugin + { + public string Id => "_invalid_"; + + public string Name { get; set; } + + public string Author => "unknown"; + + public string Description { get; set; } + + public string Version => "not loaded"; + + public List Options => null; + }; + + // Singleton pattern + private static PluginManager _singleton; + public static PluginManager AsInstance { + get { + if (_singleton == null) { + _singleton = new PluginManager(); + Reload(); + } + return _singleton; + } + } + + // We don't call Reload() in the constructor to avoid infinite recursion + private PluginManager() { } + + // Error handler called when a plugin throws an exception + // This should be hooked by the client consuming the Il2CppInspector class library + // If not used, all exceptions are suppressed (which is probably really bad) + public static event EventHandler ErrorHandler; + + // Handler called when a plugin reports a status update + // If not used, all status updates are suppressed + public static event EventHandler StatusHandler; + + // Find and load all available plugins from disk + public static void Reload() { + AsInstance.ManagedPlugins.Clear(); + + // Don't do anything if there's no plugins folder + if (!Directory.Exists(pluginFolder)) + return; + + // Get every DLL + // NOTE: Every plugin should be in its own folder together with its dependencies + var dlls = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories); + + foreach (var dll in dlls) { + // All plugin interfaces we allow for this version of Il2CppInspector + // Add new versions to allow backwards compatibility + var loader = PluginLoader.CreateFromAssemblyFile(dll, + sharedTypes: new[] { + typeof(PluginAPI.V100.IPlugin), + //typeof(PluginAPI.V101.IPlugin) + }); + + // Load plugin + try { + var asm = loader.LoadDefaultAssembly(); + + // Determine plugin version and instantiate as appropriate + foreach (var type in asm.GetTypes()) { + // Current version + if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract) { + var plugin = (IPlugin) Activator.CreateInstance(type); + AsInstance.ManagedPlugins.Add(new ManagedPlugin { Plugin = plugin, Available = true, Enabled = false }); + } + + // Add older versions here with adapters + /* + // V100 + else if (typeof(PluginAPI.V100.IPlugin).IsAssignableFrom(type) && !type.IsAbstract) { + var plugin = (PluginAPI.V100.IPlugin) Activator.CreateInstance(type); + var adapter = new PluginAPI.V101.Adapter(plugin); + Plugins.Add(new Plugin { Interface = adapter, Available = true, Enabled = false }); + }*/ + } + } + + // Problem finding all the types required to load the plugin + catch (ReflectionTypeLoadException ex) { + var name = Path.GetFileName(dll); + + // Construct disabled plugin + var plugin = new ManagedPlugin { + Plugin = null, + Available = false, + Enabled = false + }; + AsInstance.ManagedPlugins.Add(plugin); + + // Determine error + switch (ex.LoaderExceptions[0]) { + + // Type could not be found + case TypeLoadException failedType: + if (failedType.TypeName.StartsWith("Il2CppInspector.PluginAPI.")) { + + // Requires newer plugin API version + plugin.Plugin = new InvalidPlugin { Name = name, Description = "This plugin requires a newer version of Il2CppInspector" }; + Console.Error.WriteLine($"Error loading plugin {plugin.Plugin.Name}: {plugin.Plugin.Description}"); + } else { + + // Missing dependencies + plugin.Plugin = new InvalidPlugin { Name = name, Description = "This plugin has dependencies that could not be found. Check that all required DLLs are present in the plugins folder." }; + Console.Error.WriteLine($"Error loading plugin {plugin.Plugin.Name}: {plugin.Plugin.Description}"); + } + break; + + // Assembly could not be found + case FileNotFoundException failedFile: + plugin.Plugin = new InvalidPlugin { Name = name, Description = $"This plugin needs {failedFile.FileName} but the file could not be found" }; + Console.Error.WriteLine($"Error loading plugin {plugin.Plugin.Name}: {plugin.Plugin.Description}"); + break; + + // Some other type loading error + default: + throw new InvalidOperationException($"Fatal error loading plugin {name}: {ex.LoaderExceptions[0].GetType()} - {ex.LoaderExceptions[0].Message}"); + } + } + + // Ignore unmanaged DLLs + catch (BadImageFormatException) { } + + // Some other load error (probably generated by the plugin itself) + catch (Exception ex) { + var name = Path.GetFileName(dll); + + throw new InvalidOperationException($"Fatal error loading plugin {name}: {ex.GetType()} - {ex.Message}"); + } + } + } + + // Try to cast each enabled plugin to a specific interface type, and for those supporting the interface, execute the supplied delegate + // Errors will be forwarded to the error handler + internal static void Try(Action action) { + foreach (var plugin in EnabledPlugins) + if (plugin is I p) + try { + action(p); + } + catch (Exception ex) { + ErrorHandler?.Invoke(AsInstance, new PluginErrorEventArgs { Plugin = plugin, Exception = ex, Operation = typeof(I).Name }); + } + } + + // Process an incoming status update + internal static void StatusUpdate(IPlugin plugin, string text) { + StatusHandler?.Invoke(AsInstance, new PluginStatusEventArgs { Plugin = plugin, Text = text }); + } + } +}