using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Dalamud.Configuration; using Newtonsoft.Json; using Serilog; namespace Dalamud.Plugin { public class PluginManager { private readonly Dalamud dalamud; private readonly string pluginDirectory; private readonly string devPluginDirectory; private readonly PluginConfigurations pluginConfigs; private readonly Type interfaceType = typeof(IDalamudPlugin); public readonly List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface)> Plugins = new List<(IDalamudPlugin plugin, PluginDefinition def, DalamudPluginInterface PluginInterface)>(); public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory) { this.dalamud = dalamud; this.pluginDirectory = pluginDirectory; this.devPluginDirectory = devPluginDirectory; this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs")); } public void UnloadPlugins() { if (this.Plugins == null) return; for (var i = 0; i < this.Plugins.Count; i++) { this.Plugins[i].Plugin.Dispose(); } this.Plugins.Clear(); } public void LoadPlugins() { LoadPluginsAt(new DirectoryInfo(this.pluginDirectory), false); LoadPluginsAt(new DirectoryInfo(this.devPluginDirectory), true); } public void DisablePlugin(PluginDefinition definition) { var thisPlugin = this.Plugins.Where(x => x.Definition != null) .First(x => x.Definition.InternalName == definition.InternalName); var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion)); // Need to do it with Open so the file handle gets closed immediately // TODO: Don't use the ".disabled" crap, do it in a config var disabledFile = File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create); disabledFile.Close(); thisPlugin.Plugin.Dispose(); this.Plugins.Remove(thisPlugin); } public bool LoadPluginFromAssembly(FileInfo dllFile, bool raw) { Log.Information("Loading plugin at {0}", dllFile.Directory.FullName); // If this entire folder has been marked as a disabled plugin, don't even try to load anything var disabledFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".disabled")); if (disabledFile.Exists && !raw) // should raw/dev plugins really not respect this? { Log.Information("Plugin {0} is disabled.", dllFile.FullName); return false; } // read the plugin def if present - again, fail before actually trying to load the dll if there is a problem var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json")); PluginDefinition pluginDef = null; // load the definition if it exists, even for raw/developer plugins if (defJsonFile.Exists) { Log.Information("Loading definition for plugin DLL {0}", dllFile.FullName); pluginDef = JsonConvert.DeserializeObject( File.ReadAllText(defJsonFile.FullName)); if (pluginDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion && pluginDef.ApplicableVersion != "any") { Log.Information("Plugin {0} has not applicable version.", dllFile.FullName); return false; } } // but developer plugins don't require one to load else if (!raw) { Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName); return false; } // TODO: given that it exists, the pluginDef's InternalName should probably be used // as the actual assembly to load // But plugins should also probably be loaded by directory and not by looking for every dll Log.Information("Loading assembly at {0}", dllFile); // Assembly.Load() by name here will not load multiple versions with the same name, in the case of updates var pluginAssembly = Assembly.LoadFile(dllFile.FullName); if (pluginAssembly != null) { Log.Information("Loading types for {0}", pluginAssembly.FullName); var types = pluginAssembly.GetTypes(); foreach (var type in types) { if (type.IsInterface || type.IsAbstract) { continue; } if (type.GetInterface(interfaceType.FullName) != null) { if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name)) { Log.Error("Duplicate plugin found: {0}", dllFile.FullName); return false; } var plugin = (IDalamudPlugin)Activator.CreateInstance(type); // this happens for raw plugins that don't specify a PluginDefinition - just generate a dummy one to avoid crashes anywhere if (pluginDef == null) { pluginDef = new PluginDefinition { Author = "developer", Name = plugin.Name, InternalName = Path.GetFileNameWithoutExtension(dllFile.Name), AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(), Description = "", ApplicableVersion = "any", IsHide = false }; } var dalamudInterface = new DalamudPluginInterface(this.dalamud, type.Assembly.GetName().Name, this.pluginConfigs); plugin.Initialize(dalamudInterface); Log.Information("Loaded plugin: {0}", plugin.Name); this.Plugins.Add((plugin, pluginDef, dalamudInterface)); return true; } } } Log.Information("Plugin DLL {0} has no plugin interface.", dllFile.FullName); return false; } private void LoadPluginsAt(DirectoryInfo folder, bool raw) { if (folder.Exists) { Log.Information("Loading plugins at {0}", folder); var pluginDlls = folder.GetFiles("*.dll", SearchOption.AllDirectories); foreach (var dllFile in pluginDlls) { try { LoadPluginFromAssembly(dllFile, raw); } catch (Exception ex) { Log.Error(ex, $"Plugin load for {dllFile.FullName} failed."); } } } } } }