Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce IAsyncDalamudPlugin to tidy up plugin lifecycle #1905

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Dalamud/Plugin/IDalamudPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
using System.Threading.Tasks;

namespace Dalamud.Plugin;

/// <summary>
/// This interface represents a basic Dalamud plugin. All plugins have to implement this interface.
/// This interface represents a basic Dalamud plugin.
/// </summary>
[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
{
}

/// <summary>
/// This interface represents a basic Dalamud plugin that can be loaded and unloaded asynchronously.
/// </summary>
public interface IAsyncDalamudPlugin : IAsyncDisposable
{
/// <summary>Performs plugin-defined tasks associated with loading the plugin asynchronously.</summary>
/// <returns>A task that represents the asynchronous load operation.</returns>
ValueTask LoadAsync();
}
163 changes: 95 additions & 68 deletions Dalamud/Plugin/Internal/Types/LocalPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -43,7 +41,7 @@ internal class LocalPlugin : IDisposable
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
private IDalamudPlugin? instance;
private object? instance;

/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
Expand All @@ -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);
}

Expand Down Expand Up @@ -228,33 +225,19 @@ public LocalPlugin(FileInfo dllFile, LocalPluginManifest manifest)
/// <inheritdoc/>
public void Dispose()
{
var framework = Service<Framework>.GetNullable();
var configuration = Service<DalamudConfiguration>.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();
}

Expand Down Expand Up @@ -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. \"<Private>False</Private>\" 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. \"<Private>False</Private>\" 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)
Expand All @@ -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
Expand All @@ -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);
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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);

Expand All @@ -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<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();

await this.pluginLoadStateLock.WaitAsync();
try
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -667,26 +670,27 @@ private void EnsureLoader()
try
{
this.pluginAssembly = this.loader.LoadDefaultAssembly();
this.AssemblyName = this.pluginAssembly.GetName();
}
catch (Exception ex)
{
this.pluginAssembly = null;
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)
Expand All @@ -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<Framework>.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)
Expand Down
Loading