mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-31 21:03:43 +01:00
Magic the magic happen
This commit is contained in:
parent
84769ae5b7
commit
658eedca37
188 changed files with 10329 additions and 3549 deletions
415
Dalamud/Plugin/Internal/LocalPlugin.cs
Normal file
415
Dalamud/Plugin/Internal/LocalPlugin.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Plugin.Internal.Exceptions;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using McMaster.NETCore.Plugins;
|
||||
|
||||
namespace Dalamud.Plugin.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
internal class LocalPlugin : IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new("PLUGIN");
|
||||
|
||||
private readonly Dalamud dalamud;
|
||||
private readonly FileInfo manifestFile;
|
||||
private readonly FileInfo disabledFile;
|
||||
private readonly FileInfo testingFile;
|
||||
|
||||
private PluginLoader loader;
|
||||
private Assembly pluginAssembly;
|
||||
private Type pluginType;
|
||||
private IDalamudPlugin instance;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dalamud">Dalamud instance.</param>
|
||||
/// <param name="dllFile">Path to the DLL file.</param>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
public LocalPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest)
|
||||
{
|
||||
this.dalamud = dalamud;
|
||||
this.DllFile = dllFile;
|
||||
this.State = PluginState.Unloaded;
|
||||
|
||||
this.loader ??= PluginLoader.CreateFromAssemblyFile(
|
||||
this.DllFile.FullName,
|
||||
config =>
|
||||
{
|
||||
config.IsUnloadable = true;
|
||||
config.LoadInMemory = true;
|
||||
config.PreferSharedTypes = true;
|
||||
});
|
||||
|
||||
Version assemblyVersion;
|
||||
|
||||
try
|
||||
{
|
||||
// BadImageFormatException
|
||||
this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
|
||||
|
||||
// InvalidOperationException
|
||||
this.pluginType = this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
|
||||
|
||||
assemblyVersion = this.pluginAssembly.GetName().Version;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
this.pluginAssembly = null;
|
||||
this.pluginType = null;
|
||||
this.loader.Dispose();
|
||||
|
||||
Log.Debug($"Not a plugin: {this.DllFile.Name}");
|
||||
throw new InvalidPluginException(this.DllFile);
|
||||
}
|
||||
|
||||
// Files that may or may not exist
|
||||
this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
|
||||
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
|
||||
this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
|
||||
|
||||
// If the parameter manifest was null
|
||||
if (manifest == null)
|
||||
{
|
||||
this.Manifest = new LocalPluginManifest()
|
||||
{
|
||||
Author = "developer",
|
||||
Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
|
||||
InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
|
||||
AssemblyVersion = assemblyVersion,
|
||||
Description = string.Empty,
|
||||
ApplicableVersion = GameVersion.Any,
|
||||
DalamudApiLevel = PluginManager.DalamudApiLevel,
|
||||
IsHide = false,
|
||||
};
|
||||
|
||||
// Save the manifest to disk so there won't be any problems later.
|
||||
// We'll update the name property after it can be retrieved from the instance.
|
||||
var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
|
||||
this.Manifest.Save(manifestFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Manifest = manifest;
|
||||
}
|
||||
|
||||
// This bit converts from ".disabled" functionality to using the manifest.
|
||||
if (this.disabledFile.Exists)
|
||||
{
|
||||
this.Manifest.Disabled = true;
|
||||
this.disabledFile.Delete();
|
||||
}
|
||||
|
||||
// This bit converts from ".testing" functionality to using the manifest.
|
||||
if (this.testingFile.Exists)
|
||||
{
|
||||
this.Manifest.Testing = true;
|
||||
this.testingFile.Delete();
|
||||
}
|
||||
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
|
||||
/// </summary>
|
||||
public DalamudPluginInterface DalamudInterface { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin DLL.
|
||||
/// </summary>
|
||||
public FileInfo DllFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin manifest, if one exists.
|
||||
/// </summary>
|
||||
public LocalPluginManifest Manifest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state of the plugin.
|
||||
/// </summary>
|
||||
public PluginState State { get; protected set; } = PluginState.Unloaded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AssemblyName plugin, populated during <see cref="Load(bool)"/>.
|
||||
/// </summary>
|
||||
/// <returns>Plugin type.</returns>
|
||||
public AssemblyName AssemblyName { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
|
||||
/// </summary>
|
||||
public string Name => this.instance?.Name ?? this.Manifest.Name ?? this.DllFile.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is loaded and running.
|
||||
/// </summary>
|
||||
public bool IsLoaded => this.State == PluginState.Loaded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is disabled.
|
||||
/// </summary>
|
||||
public bool IsDisabled => this.Manifest.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is for testing use only.
|
||||
/// </summary>
|
||||
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin is dev plugin.
|
||||
/// </summary>
|
||||
public bool IsDev => this is LocalDevPlugin;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.instance?.Dispose();
|
||||
this.instance = null;
|
||||
|
||||
this.DalamudInterface.Dispose();
|
||||
this.DalamudInterface = null;
|
||||
|
||||
this.pluginType = null;
|
||||
this.pluginAssembly = null;
|
||||
|
||||
this.loader?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load this plugin.
|
||||
/// </summary>
|
||||
/// <param name="reloading">Load while reloading.</param>
|
||||
public void Load(bool reloading = false)
|
||||
{
|
||||
// Allowed: Unloaded
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.InProgress:
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
|
||||
case PluginState.Loaded:
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
|
||||
case PluginState.LoadError:
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
|
||||
case PluginState.UnloadError:
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
|
||||
}
|
||||
|
||||
if (this.Manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
|
||||
|
||||
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel)
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
|
||||
|
||||
if (this.Manifest.Disabled)
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
|
||||
|
||||
this.State = PluginState.InProgress;
|
||||
Log.Information($"Loading {this.DllFile.Name}");
|
||||
|
||||
try
|
||||
{
|
||||
this.loader ??= PluginLoader.CreateFromAssemblyFile(
|
||||
this.DllFile.FullName,
|
||||
config =>
|
||||
{
|
||||
config.IsUnloadable = true;
|
||||
config.LoadInMemory = true;
|
||||
config.PreferSharedTypes = true;
|
||||
});
|
||||
|
||||
if (reloading)
|
||||
{
|
||||
this.loader.Reload();
|
||||
}
|
||||
|
||||
// Load the assembly
|
||||
this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
|
||||
|
||||
this.AssemblyName = this.pluginAssembly.GetName();
|
||||
|
||||
// Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
|
||||
this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
|
||||
|
||||
// Check for any loaded plugins with the same assembly name
|
||||
var assemblyName = this.pluginAssembly.GetName().Name;
|
||||
foreach (var otherPlugin in this.dalamud.PluginManager.InstalledPlugins)
|
||||
{
|
||||
// During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
|
||||
if (otherPlugin == this || otherPlugin.instance == null)
|
||||
continue;
|
||||
|
||||
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
|
||||
if (otherPluginAssemblyName == assemblyName)
|
||||
{
|
||||
this.State = PluginState.Unloaded;
|
||||
Log.Debug($"Duplicate assembly: {this.Name}");
|
||||
|
||||
throw new DuplicatePluginException(assemblyName);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the location for the Location and CodeBase patches
|
||||
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
|
||||
|
||||
// Instantiate and initialize
|
||||
this.instance = Activator.CreateInstance(this.pluginType) as IDalamudPlugin;
|
||||
|
||||
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
|
||||
if (this.instance.Name != this.Manifest.Name)
|
||||
{
|
||||
this.Manifest.Name = this.instance.Name;
|
||||
this.Manifest.Save(this.manifestFile);
|
||||
}
|
||||
|
||||
this.DalamudInterface = new DalamudPluginInterface(this.dalamud, this.pluginAssembly.GetName().Name, this.DllFile.FullName);
|
||||
|
||||
if (this.IsDev)
|
||||
{
|
||||
// Inherit LPL's AssemblyLocation functionality
|
||||
try
|
||||
{
|
||||
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
this.instance.GetType()
|
||||
?.GetProperty("AssemblyLocation", bindingFlags)
|
||||
?.SetValue(this.instance, this.DllFile.FullName);
|
||||
this.instance.GetType()
|
||||
?.GetMethod("SetLocation", bindingFlags)
|
||||
?.Invoke(this.instance, new object[] { this.DllFile.FullName });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
this.instance.Initialize(this.DalamudInterface);
|
||||
|
||||
this.State = PluginState.Loaded;
|
||||
Log.Information($"Finished loading {this.DllFile.Name}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.State = PluginState.LoadError;
|
||||
Log.Error(ex, $"Error while loading {this.Name}");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
|
||||
/// in the plugin list until it has been actually disposed.
|
||||
/// </summary>
|
||||
/// <param name="reloading">Unload while reloading.</param>
|
||||
public void Unload(bool reloading = false)
|
||||
{
|
||||
// Allowed: Loaded
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.InProgress:
|
||||
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
|
||||
case PluginState.LoadError:
|
||||
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, load previously faulted, unload first");
|
||||
case PluginState.Unloaded:
|
||||
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
|
||||
case PluginState.UnloadError:
|
||||
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.State = PluginState.InProgress;
|
||||
Log.Information($"Unloading {this.DllFile.Name}");
|
||||
|
||||
this.instance?.Dispose();
|
||||
this.instance = null;
|
||||
|
||||
this.DalamudInterface?.Dispose();
|
||||
this.DalamudInterface = null;
|
||||
|
||||
this.pluginType = null;
|
||||
this.pluginAssembly = null;
|
||||
|
||||
if (!reloading)
|
||||
{
|
||||
this.loader?.Dispose();
|
||||
this.loader = null;
|
||||
}
|
||||
|
||||
this.State = PluginState.Unloaded;
|
||||
Log.Information($"Finished unloading {this.DllFile.Name}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.State = PluginState.UnloadError;
|
||||
Log.Error(ex, $"Error while unloading {this.Name}");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload this plugin.
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
this.Unload(true);
|
||||
this.Load(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revert a disable. Must be unloaded first, does not load.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.InProgress:
|
||||
case PluginState.Loaded:
|
||||
case PluginState.LoadError:
|
||||
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
|
||||
}
|
||||
|
||||
if (!this.Manifest.Disabled)
|
||||
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
|
||||
|
||||
this.Manifest.Disabled = false;
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable this plugin, must be unloaded first.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.InProgress:
|
||||
case PluginState.Loaded:
|
||||
case PluginState.LoadError:
|
||||
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
|
||||
}
|
||||
|
||||
if (this.Manifest.Disabled)
|
||||
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
|
||||
|
||||
this.Manifest.Disabled = true;
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
private void SaveManifest() => this.Manifest.Save(this.manifestFile);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue