refactor: new code style in PluginManager.cs

This commit is contained in:
goat 2021-04-01 21:11:32 +02:00
parent 03b1927434
commit aca3da09b1

View file

@ -1,18 +1,25 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Dalamud.Configuration; using Dalamud.Configuration;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Dalamud.Plugin namespace Dalamud.Plugin
{ {
internal class PluginManager { /// <summary>
public static int DALAMUD_API_LEVEL = 2; /// Class responsible for loading and unloading plugins.
/// </summary>
internal class PluginManager
{
/// <summary>
/// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
/// </summary>
public const int DalamudApiLevel = 2;
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private readonly string pluginDirectory; private readonly string pluginDirectory;
@ -22,15 +29,23 @@ namespace Dalamud.Plugin
private readonly Type interfaceType = typeof(IDalamudPlugin); private readonly Type interfaceType = typeof(IDalamudPlugin);
public readonly List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> Plugins = new List<(IDalamudPlugin plugin, PluginDefinition def, DalamudPluginInterface PluginInterface, bool IsRaw)>(); /// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
public List<(string SourcePluginName, string SubPluginName, Action<ExpandoObject> SubAction)> IpcSubscriptions = new List<(string SourcePluginName, string SubPluginName, Action<ExpandoObject> SubAction)>(); /// </summary>
/// <param name="dalamud">The <see cref="Dalamud"/> instance to load plugins with.</param>
public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory) { /// <param name="pluginDirectory">The directory for regular plugins.</param>
/// <param name="devPluginDirectory">The directory for dev plugins.</param>
public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory)
{
this.dalamud = dalamud; this.dalamud = dalamud;
this.pluginDirectory = pluginDirectory; this.pluginDirectory = pluginDirectory;
this.devPluginDirectory = devPluginDirectory; this.devPluginDirectory = devPluginDirectory;
this.Plugins =
new List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface,
bool IsRaw)>();
this.IpcSubscriptions = new List<(string SourcePluginName, string SubPluginName, Action<ExpandoObject> SubAction)>();
this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs")); this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs"));
// Try to load missing assemblies from the local directory of the requesting assembly // Try to load missing assemblies from the local directory of the requesting assembly
@ -38,49 +53,78 @@ namespace Dalamud.Plugin
// This handler should only be invoked on things that fail regular lookups, but it *is* global to this appdomain // This handler should only be invoked on things that fail regular lookups, but it *is* global to this appdomain
AppDomain.CurrentDomain.AssemblyResolve += (object source, ResolveEventArgs e) => AppDomain.CurrentDomain.AssemblyResolve += (object source, ResolveEventArgs e) =>
{ {
try { try
{
Log.Debug($"Resolving missing assembly {e.Name}"); Log.Debug($"Resolving missing assembly {e.Name}");
// This looks weird but I'm pretty sure it's actually correct. Pretty sure. Probably. // This looks weird but I'm pretty sure it's actually correct. Pretty sure. Probably.
var assemblyPath = Path.Combine(Path.GetDirectoryName(e.RequestingAssembly.Location), var assemblyPath = Path.Combine(
new AssemblyName(e.Name).Name + ".dll"); Path.GetDirectoryName(e.RequestingAssembly.Location),
if (!File.Exists(assemblyPath)) { new AssemblyName(e.Name).Name + ".dll");
if (!File.Exists(assemblyPath))
{
Log.Error($"Assembly not found at {assemblyPath}"); Log.Error($"Assembly not found at {assemblyPath}");
return null; return null;
} }
return Assembly.LoadFrom(assemblyPath); return Assembly.LoadFrom(assemblyPath);
} catch(Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Could not load assembly " + e.Name); Log.Error(ex, "Could not load assembly " + e.Name);
return null; return null;
} }
}; };
} }
public void UnloadPlugins() { /// <summary>
/// Gets a list of all loaded plugins.
/// </summary>
public List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> Plugins { get; private set; }
/// <summary>
/// Gets a list of all IPC subscriptions.
/// </summary>
public List<(string SourcePluginName, string SubPluginName, Action<ExpandoObject> SubAction)> IpcSubscriptions { get; private set; }
/// <summary>
/// Unload all plugins.
/// </summary>
public void UnloadPlugins()
{
if (this.Plugins == null) if (this.Plugins == null)
return; return;
for (var i = 0; i < this.Plugins.Count; i++) { for (var i = 0; i < this.Plugins.Count; i++)
{
this.Plugins[i].Plugin.Dispose(); this.Plugins[i].Plugin.Dispose();
} }
this.Plugins.Clear(); this.Plugins.Clear();
} }
public void LoadPlugins() { /// <summary>
var loadDirectories = new List<(DirectoryInfo dirInfo, bool isRaw)> { /// Load all regular and dev plugins.
/// </summary>
public void LoadPlugins()
{
var loadDirectories = new List<(DirectoryInfo dirInfo, bool isRaw)>
{
(new DirectoryInfo(this.pluginDirectory), false), (new DirectoryInfo(this.pluginDirectory), false),
(new DirectoryInfo(this.devPluginDirectory), true) (new DirectoryInfo(this.devPluginDirectory), true),
}; };
var pluginDefs = new List<(FileInfo dllFile, PluginDefinition definition, bool isRaw)>(); var pluginDefs = new List<(FileInfo dllFile, PluginDefinition definition, bool isRaw)>();
foreach (var (dirInfo, isRaw) in loadDirectories) { foreach (var (dirInfo, isRaw) in loadDirectories)
{
if (!dirInfo.Exists) continue; if (!dirInfo.Exists) continue;
var pluginDlls = dirInfo.GetFiles("*.dll", SearchOption.AllDirectories).Where(x => x.Extension == ".dll"); var pluginDlls = dirInfo.GetFiles("*.dll", SearchOption.AllDirectories).Where(x => x.Extension == ".dll");
// Preload definitions to be able to determine load order // Preload definitions to be able to determine load order
foreach (var dllFile in pluginDlls) { foreach (var dllFile in pluginDlls)
{
var defJson = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json")); var defJson = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
PluginDefinition def = null; PluginDefinition def = null;
if (defJson.Exists) if (defJson.Exists)
@ -91,21 +135,27 @@ namespace Dalamud.Plugin
// Sort for load order - unloaded definitions have default priority of 0 // Sort for load order - unloaded definitions have default priority of 0
pluginDefs.Sort( pluginDefs.Sort(
(info1, info2) => { (info1, info2) =>
{
var prio1 = info1.definition?.LoadPriority ?? 0; var prio1 = info1.definition?.LoadPriority ?? 0;
var prio2 = info2.definition?.LoadPriority ?? 0; var prio2 = info2.definition?.LoadPriority ?? 0;
return prio2.CompareTo(prio1); return prio2.CompareTo(prio1);
}); });
// Pass preloaded definitions to LoadPluginFromAssembly, because we already loaded them anyways // Pass preloaded definitions to LoadPluginFromAssembly, because we already loaded them anyways
foreach (var (dllFile, definition, isRaw) in pluginDefs) { foreach (var (dllFile, definition, isRaw) in pluginDefs)
try { {
LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, true, definition); try
{
this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, true, definition);
} }
catch (Exception ex) { catch (Exception ex)
{
Log.Error(ex, $"Plugin load for {dllFile.FullName} failed."); Log.Error(ex, $"Plugin load for {dllFile.FullName} failed.");
if (ex is ReflectionTypeLoadException typeLoadException) { if (ex is ReflectionTypeLoadException typeLoadException)
foreach (var exception in typeLoadException.LoaderExceptions) { {
foreach (var exception in typeLoadException.LoaderExceptions)
{
Log.Error(exception, "LoaderException:"); Log.Error(exception, "LoaderException:");
} }
} }
@ -113,7 +163,12 @@ namespace Dalamud.Plugin
} }
} }
public void DisablePlugin(PluginDefinition definition) { /// <summary>
/// Disable/unload a single plugin.
/// </summary>
/// <param name="definition">The plugin definition of the plugin to be disabled/unloaded.</param>
public void DisablePlugin(PluginDefinition definition)
{
var thisPlugin = this.Plugins.Where(x => x.Definition != null) var thisPlugin = this.Plugins.Where(x => x.Definition != null)
.First(x => x.Definition.InternalName == definition.InternalName); .First(x => x.Definition.InternalName == definition.InternalName);
@ -121,11 +176,15 @@ namespace Dalamud.Plugin
// Need to do it with Open so the file handle gets closed immediately // 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 // TODO: Don't use the ".disabled" crap, do it in a config
try { try
{
File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create).Close(); File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create).Close();
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Could not create the .disabled file, disabling all versions..."); Log.Error(ex, "Could not create the .disabled file, disabling all versions...");
foreach (var version in outputDir.Parent.GetDirectories()) { foreach (var version in outputDir.Parent.GetDirectories())
{
if (!File.Exists(Path.Combine(version.FullName, ".disabled"))) if (!File.Exists(Path.Combine(version.FullName, ".disabled")))
File.Open(Path.Combine(version.FullName, ".disabled"), FileMode.Create).Close(); File.Open(Path.Combine(version.FullName, ".disabled"), FileMode.Create).Close();
} }
@ -136,19 +195,32 @@ namespace Dalamud.Plugin
this.Plugins.Remove(thisPlugin); this.Plugins.Remove(thisPlugin);
} }
public bool LoadPluginFromAssembly(FileInfo dllFile, bool isRaw, PluginLoadReason reason, bool preloaded = false, PluginDefinition preloadedDef = null) { /// <summary>
/// Load a plugin from an assembly.
/// </summary>
/// <param name="dllFile">The <see cref="FileInfo"/> associated with the main assembly of this plugin.</param>
/// <param name="isRaw">Whether or not the plugin is a dev plugin.</param>
/// <param name="reason">The reason this plugin was loaded.</param>
/// <param name="preloaded">Whether or not to skip loading a definition from a file path.</param>
/// <param name="preloadedDef">The already loaded definition, when <paramref name="preloaded"/> is set to true.</param>
/// <returns>Whether or not the plugin was loaded successfully.</returns>
public bool LoadPluginFromAssembly(FileInfo dllFile, bool isRaw, PluginLoadReason reason, bool preloaded = false, PluginDefinition preloadedDef = null)
{
Log.Information("Loading plugin at {0}", dllFile.Directory.FullName); 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 // 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")); var disabledFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".disabled"));
if (disabledFile.Exists && !isRaw) // should raw/dev plugins really not respect this?
// should raw/dev plugins really not respect this?
if (disabledFile.Exists && !isRaw)
{ {
Log.Information("Plugin {0} is disabled.", dllFile.FullName); Log.Information("Plugin {0} is disabled.", dllFile.FullName);
return false; return false;
} }
var testingFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".testing")); var testingFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".testing"));
if (testingFile.Exists && !this.dalamud.Configuration.DoPluginTest) { if (testingFile.Exists && !this.dalamud.Configuration.DoPluginTest)
{
Log.Information("Plugin {0} was testing, but testing is disabled.", dllFile.FullName); Log.Information("Plugin {0} was testing, but testing is disabled.", dllFile.FullName);
return false; return false;
} }
@ -156,12 +228,14 @@ namespace Dalamud.Plugin
PluginDefinition pluginDef = null; PluginDefinition pluginDef = null;
// Preloaded // Preloaded
if (preloaded) { if (preloaded)
{
if (preloadedDef == null && !isRaw) if (preloadedDef == null && !isRaw)
{ {
Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName); Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName);
return false; return false;
} }
if (preloadedDef != null && if (preloadedDef != null &&
preloadedDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion && preloadedDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion &&
preloadedDef.ApplicableVersion != "any") preloadedDef.ApplicableVersion != "any")
@ -169,8 +243,11 @@ namespace Dalamud.Plugin
Log.Information("Plugin {0} has not applicable version.", dllFile.FullName); Log.Information("Plugin {0} has not applicable version.", dllFile.FullName);
return false; return false;
} }
pluginDef = preloadedDef; pluginDef = preloadedDef;
} else { }
else
{
// read the plugin def if present - again, fail before actually trying to load the dll if there is a problem // 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")); var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
@ -189,6 +266,7 @@ namespace Dalamud.Plugin
return false; return false;
} }
} }
// but developer plugins don't require one to load // but developer plugins don't require one to load
else if (!isRaw) else if (!isRaw)
{ {
@ -200,7 +278,6 @@ namespace Dalamud.Plugin
// TODO: given that it exists, the pluginDef's InternalName should probably be used // TODO: given that it exists, the pluginDef's InternalName should probably be used
// as the actual assembly to load // as the actual assembly to load
// But plugins should also probably be loaded by directory and not by looking for every dll // But plugins should also probably be loaded by directory and not by looking for every dll
Log.Information("Loading assembly at {0}", dllFile); 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 // Assembly.Load() by name here will not load multiple versions with the same name, in the case of updates
@ -215,7 +292,7 @@ namespace Dalamud.Plugin
continue; continue;
} }
if (type.GetInterface(interfaceType.FullName) != null) if (type.GetInterface(this.interfaceType.FullName) != null)
{ {
if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name)) if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name))
{ {
@ -228,33 +305,38 @@ namespace Dalamud.Plugin
var plugin = (IDalamudPlugin)Activator.CreateInstance(type); 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 // this happens for raw plugins that don't specify a PluginDefinition - just generate a dummy one to avoid crashes anywhere
pluginDef ??= new PluginDefinition{ pluginDef ??= new PluginDefinition
{
Author = "developer", Author = "developer",
Name = plugin.Name, Name = plugin.Name,
InternalName = Path.GetFileNameWithoutExtension(dllFile.Name), InternalName = Path.GetFileNameWithoutExtension(dllFile.Name),
AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(), AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(),
Description = "", Description = string.Empty,
ApplicableVersion = "any", ApplicableVersion = "any",
IsHide = false, IsHide = false,
DalamudApiLevel = DALAMUD_API_LEVEL DalamudApiLevel = DalamudApiLevel,
}; };
if (pluginDef.InternalName == "PingPlugin" && pluginDef.AssemblyVersion == "1.11.0.0") { if (pluginDef.InternalName == "PingPlugin" && pluginDef.AssemblyVersion == "1.11.0.0")
{
Log.Error("Banned PingPlugin"); Log.Error("Banned PingPlugin");
return false; return false;
} }
if (pluginDef.InternalName == "FPSPlugin" && pluginDef.AssemblyVersion == "1.4.2.0") { if (pluginDef.InternalName == "FPSPlugin" && pluginDef.AssemblyVersion == "1.4.2.0")
{
Log.Error("Banned PingPlugin"); Log.Error("Banned PingPlugin");
return false; return false;
} }
if (pluginDef.InternalName == "SonarPlugin" && pluginDef.AssemblyVersion == "0.1.3.1") { if (pluginDef.InternalName == "SonarPlugin" && pluginDef.AssemblyVersion == "0.1.3.1")
{
Log.Error("Banned SonarPlugin"); Log.Error("Banned SonarPlugin");
return false; return false;
} }
if (pluginDef.DalamudApiLevel < DALAMUD_API_LEVEL) { if (pluginDef.DalamudApiLevel < DalamudApiLevel)
{
Log.Error("Incompatible API level: {0}", dllFile.FullName); Log.Error("Incompatible API level: {0}", dllFile.FullName);
return false; return false;
} }
@ -276,9 +358,13 @@ namespace Dalamud.Plugin
return false; return false;
} }
public void ReloadPlugins() { /// <summary>
UnloadPlugins(); /// Unload and reload all plugins.
LoadPlugins(); /// </summary>
public void ReloadPlugins()
{
this.UnloadPlugins();
this.LoadPlugins();
} }
} }
} }