diff --git a/Dalamud/Plugin/IDalamudPlugin.cs b/Dalamud/Plugin/IDalamudPlugin.cs
index ebf39708f..332d0daa6 100644
--- a/Dalamud/Plugin/IDalamudPlugin.cs
+++ b/Dalamud/Plugin/IDalamudPlugin.cs
@@ -1,8 +1,21 @@
+using System.Threading.Tasks;
+
namespace Dalamud.Plugin;
///
-/// This interface represents a basic Dalamud plugin. All plugins have to implement this interface.
+/// This interface represents a basic Dalamud plugin.
///
+[Obsolete("Use IAsyncDalamudPlugin instead and make sure that your plugin can load and unload asynchronously. This interface will be removed in a future version. Please refer to http://ooo for more information.")]
public interface IDalamudPlugin : IDisposable
{
}
+
+///
+/// This interface represents a basic Dalamud plugin that can be loaded and unloaded asynchronously.
+///
+public interface IAsyncDalamudPlugin : IAsyncDisposable
+{
+ /// Performs plugin-defined tasks associated with loading the plugin asynchronously.
+ /// A task that represents the asynchronous load operation.
+ ValueTask LoadAsync();
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
index 4bc2add70..33bb97345 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -6,7 +6,6 @@
using Dalamud.Configuration.Internal;
using Dalamud.Game;
-using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
@@ -15,7 +14,6 @@
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types.Manifest;
-using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Types;
@@ -43,7 +41,7 @@ internal class LocalPlugin : IDisposable
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
- private IDalamudPlugin? instance;
+ private object? instance;
///
/// Initializes a new instance of the class.
@@ -56,7 +54,6 @@ public LocalPlugin(FileInfo dllFile, LocalPluginManifest manifest)
{
// Could this be done another way? Sure. It is an extremely common source
// of errors in the log through, and should never be loaded as a plugin.
- Log.Error($"Not a plugin: {dllFile.FullName}");
throw new InvalidPluginException(dllFile);
}
@@ -228,33 +225,19 @@ public LocalPlugin(FileInfo dllFile, LocalPluginManifest manifest)
///
public void Dispose()
{
- var framework = Service.GetNullable();
var configuration = Service.Get();
- var didPluginDispose = false;
- if (this.instance != null)
+ // TODO: Would it not be safer to just call UnloadAsync() here?
+ var needsDispose = this.instance != null;
+ if (needsDispose)
{
- didPluginDispose = true;
- if (this.manifest.CanUnloadAsync || framework == null)
- this.instance.Dispose();
- else
- framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait();
-
- this.instance = null;
+ this.UnloadAndDisposeInstanceAsync().Wait();
+ this.UnloadAndDisposeState();
+
+ if (this.loader != null)
+ Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
}
- this.DalamudInterface?.Dispose();
-
- this.DalamudInterface = null;
-
- this.ServiceScope?.Dispose();
- this.ServiceScope = null;
-
- this.pluginType = null;
- this.pluginAssembly = null;
-
- if (this.loader != null && didPluginDispose)
- Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
}
@@ -336,32 +319,31 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it");
this.State = PluginState.Loading;
- Log.Information($"Loading {this.DllFile.Name}");
+ Log.Information("Now loading {InternalName} at {DllFile}", this.InternalName, this.DllFile.FullName);
this.EnsureLoader();
-
+
if (this.DllFile.DirectoryName != null &&
File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
+ // ReSharper disable LogMessageIsSentenceProblem
Log.Error(
- "==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====",
+ "==== IMPORTANT MESSAGE TO {Author}, THE DEVELOPER OF {Plugin} ====",
this.manifest.Author!,
this.manifest.InternalName);
- Log.Error(
- "YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
- Log.Error(
- "You may not be able to load your plugin. \"False\" needs to be set in your csproj.");
- Log.Error(
- "If you are using ILMerge, do not merge anything other than your direct dependencies.");
+ Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
+ Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj.");
+ Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
- Log.Error(
- "Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
+ Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
+ // ReSharper restore LogMessageIsSentenceProblem
}
this.HasEverStartedLoad = true;
-
- this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
-
+
+ if (this.loader == null)
+ throw new Exception("Loader is null");
+
if (reloading || this.IsDev)
{
if (this.IsDev)
@@ -377,17 +359,20 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
this.loader.Reload();
}
- // Load the assembly
- this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
-
- this.AssemblyName = this.pluginAssembly.GetName();
+ if (this.pluginAssembly == null)
+ throw new Exception("Plugin assembly is null");
+
+ if (this.pluginType == null)
+ throw new Exception("Plugin type is null");
+ if (this.AssemblyName == null)
+ throw new Exception("Assembly name is null");
+
// 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)));
+ this.pluginType ??= FindPluginImpl(this.pluginAssembly);
// Check for any loaded plugins with the same assembly name
- var assemblyName = this.pluginAssembly.GetName().Name;
+ var assemblyName = this.AssemblyName.Name;
foreach (var otherPlugin in pluginManager.InstalledPlugins)
{
// During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
@@ -399,8 +384,6 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{
this.State = PluginState.Unloaded;
- Log.Debug($"Duplicate assembly: {this.Name}");
-
throw new DuplicatePluginException(assemblyName);
}
}
@@ -417,17 +400,34 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
try
{
- if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1)
+ if (this.pluginType.IsAssignableTo(typeof(IAsyncDalamudPlugin)))
{
- this.instance = await framework.RunOnFrameworkThread(
- () => this.ServiceScope.CreateAsync(
- this.pluginType!,
- this.DalamudInterface!)) as IDalamudPlugin;
+ var asyncInstance =
+ await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IAsyncDalamudPlugin;
+
+ this.instance = asyncInstance;
+
+ // Caught below, so we just check here
+ if (asyncInstance != null)
+ await asyncInstance.LoadAsync();
}
else
{
- this.instance =
- await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
+ if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1)
+ {
+ var newInstance = await framework.RunOnFrameworkThread(
+ () => this.ServiceScope.CreateAsync(
+ this.pluginType!,
+ this.DalamudInterface!))
+ .ConfigureAwait(false);
+
+ this.instance = newInstance as IDalamudPlugin;
+ }
+ else
+ {
+ this.instance =
+ await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
+ }
}
}
catch (Exception ex)
@@ -455,7 +455,7 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
// If a precondition fails, don't record it as an error, as it isn't really.
if (ex is PluginPreconditionFailedException)
- Log.Warning(ex.Message);
+ Log.Warning(ex, "Precondition failed while loading {PluginName}", this.InternalName);
else
Log.Error(ex, "Error while loading {PluginName}", this.InternalName);
@@ -477,7 +477,6 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true)
{
var configuration = Service.Get();
- var framework = Service.GetNullable();
await this.pluginLoadStateLock.WaitAsync();
try
@@ -509,10 +508,7 @@ public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispo
try
{
- if (this.manifest.CanUnloadAsync || framework == null)
- this.instance?.Dispose();
- else
- await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
+ await this.UnloadAndDisposeInstanceAsync();
}
catch (Exception e)
{
@@ -628,6 +624,13 @@ public void ScheduleDeletion(bool status = true)
protected virtual void OnPreReload()
{
}
+
+ private static Type? FindPluginImpl(Assembly assembly)
+ {
+ return assembly.GetTypes().FirstOrDefault(
+ type => type.IsAssignableTo(typeof(IDalamudPlugin)) ||
+ type.IsAssignableTo(typeof(IAsyncDalamudPlugin)));
+ }
private static void SetupLoaderConfig(LoaderConfig config)
{
@@ -667,6 +670,7 @@ private void EnsureLoader()
try
{
this.pluginAssembly = this.loader.LoadDefaultAssembly();
+ this.AssemblyName = this.pluginAssembly.GetName();
}
catch (Exception ex)
{
@@ -674,19 +678,19 @@ private void EnsureLoader()
this.pluginType = null;
this.loader.Dispose();
- Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}");
+ Log.Error(ex, "Not a plugin: {DllFile}", this.DllFile.FullName);
throw new InvalidPluginException(this.DllFile);
}
try
{
- this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
+ this.pluginType = FindPluginImpl(this.pluginAssembly);
}
catch (ReflectionTypeLoadException ex)
{
- Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}");
- // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error.
- this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin)));
+ Log.Error(ex, "Could not load one or more types when searching for IDalamudPlugin for {InternalName} ({DllFile})",
+ this.InternalName, this.DllFile.FullName);
+ throw;
}
if (this.pluginType == default)
@@ -695,11 +699,34 @@ private void EnsureLoader()
this.pluginType = null;
this.loader.Dispose();
- Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
+ Log.Error("Nothing inherits from IDalamudPlugin in {DllFile}", this.DllFile.FullName);
throw new InvalidPluginException(this.DllFile);
}
}
+ private async Task UnloadAndDisposeInstanceAsync()
+ {
+ var framework = Service.Get();
+
+ switch (this.instance)
+ {
+ // Async plugins always unload async.
+ case IAsyncDalamudPlugin asyncInstance:
+ await asyncInstance.DisposeAsync();
+ break;
+
+ // Sync plugins that can unload async will unload async, if we are in off the main thread.
+ case IDalamudPlugin syncInstance when this.manifest.CanUnloadAsync || framework == null:
+ syncInstance.Dispose();
+ break;
+
+ // Otherwise, we need to run the dispose on the main thread for legacy reasons.
+ case IDalamudPlugin syncInstance:
+ await framework.RunOnFrameworkThread(() => syncInstance.Dispose()).ConfigureAwait(false);
+ break;
+ }
+ }
+
private void UnloadAndDisposeState()
{
if (this.instance != null)