Improvements (#903)

This commit is contained in:
kizer 2022-06-29 18:51:40 +09:00 committed by GitHub
parent e9cd7e0273
commit 716736f022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1809 additions and 872 deletions

View file

@ -15,6 +15,7 @@ using Dalamud.Game.Text.Sanitizer;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc;

View file

@ -38,6 +38,11 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
public const int DalamudApiLevel = 6;
/// <summary>
/// Default time to wait between plugin unload and plugin assembly unload.
/// </summary>
public const int PluginWaitBeforeFreeDefault = 500;
private static readonly ModuleLog Log = new("PLUGINM");
private readonly object pluginListLock = new();
@ -207,16 +212,43 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <inheritdoc/>
public void Dispose()
{
foreach (var plugin in this.InstalledPlugins)
if (this.InstalledPlugins.Any())
{
try
// Unload them first, just in case some of plugin codes are still running via callbacks initiated externally.
foreach (var plugin in this.InstalledPlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
{
plugin.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, $"Error disposing {plugin.Name}");
try
{
plugin.UnloadAsync(true, false).Wait();
}
catch (Exception ex)
{
Log.Error(ex, $"Error unloading {plugin.Name}");
}
}
Task.WaitAll(this.InstalledPlugins
.Where(plugin => plugin.Manifest.CanUnloadAsync)
.Select(plugin => Task.Run(async () =>
{
try
{
await plugin.UnloadAsync(true, false);
}
catch (Exception ex)
{
Log.Error(ex, $"Error unloading {plugin.Name}");
}
})).ToArray());
// Just in case plugins still have tasks running that they didn't cancel when they should have,
// give them some time to complete it.
Thread.Sleep(this.configuration.PluginWaitBeforeFree ?? PluginWaitBeforeFreeDefault);
// Now that we've waited enough, dispose the whole plugin.
// Since plugins should have been unloaded above, this should be done quickly.
foreach (var plugin in this.InstalledPlugins)
plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log);
}
this.assemblyLocationMonoHook?.Dispose();
@ -891,7 +923,7 @@ internal partial class PluginManager : IDisposable, IServiceType
{
try
{
plugin.Unload();
await plugin.UnloadAsync();
}
catch (Exception ex)
{
@ -963,23 +995,30 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <exception cref="Exception">Throws if the plugin is still loading/unloading.</exception>
public void DeleteConfiguration(LocalPlugin plugin)
/// <returns>The task.</returns>
public async Task DeleteConfigurationAsync(LocalPlugin plugin)
{
if (plugin.State == PluginState.InProgress)
if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloaded)
throw new Exception("Cannot delete configuration for a loading/unloading plugin");
if (plugin.IsLoaded)
plugin.Unload();
await plugin.UnloadAsync();
// Let's wait so any handles on files in plugin configurations can be closed
Thread.Sleep(500);
this.PluginConfigs.Delete(plugin.Name);
Thread.Sleep(500);
for (var waitUntil = Environment.TickCount64 + 1000; Environment.TickCount64 < waitUntil;)
{
try
{
this.PluginConfigs.Delete(plugin.Name);
break;
}
catch (IOException)
{
await Task.Delay(100);
}
}
// Let's indicate "installer" here since this is supposed to be a fresh install
plugin.LoadAsync(PluginLoadReason.Installer).Wait();
await plugin.LoadAsync(PluginLoadReason.Installer);
}
/// <summary>

View file

@ -2,10 +2,13 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
@ -27,6 +30,8 @@ internal class LocalPlugin : IDisposable
private readonly FileInfo disabledFile;
private readonly FileInfo testingFile;
private readonly SemaphoreSlim pluginLoadStateLock = new(1);
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
@ -208,8 +213,20 @@ internal class LocalPlugin : IDisposable
/// <inheritdoc/>
public void Dispose()
{
this.instance?.Dispose();
this.instance = null;
var framework = Service<Framework>.GetNullable();
var configuration = Service<DalamudConfiguration>.Get();
var didPluginDispose = false;
if (this.instance != null)
{
didPluginDispose = true;
if (this.Manifest.CanUnloadAsync || framework == null)
this.instance.Dispose();
else
framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait();
this.instance = null;
}
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
@ -217,6 +234,8 @@ internal class LocalPlugin : IDisposable
this.pluginType = null;
this.pluginAssembly = null;
if (this.loader != null && didPluginDispose)
Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
}
@ -228,54 +247,73 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var configuration = await Service<DalamudConfiguration>.GetAsync();
var framework = await Service<Framework>.GetAsync();
var ioc = await Service<ServiceContainer>.GetAsync();
var pluginManager = await Service<PluginManager>.GetAsync();
var startInfo = await Service<DalamudStartInfo>.GetAsync();
// 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");
case PluginState.Unloaded:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
// UiBuilder constructor requires the following two.
await Service<InterfaceManager>.GetAsync();
await Service<GameFontManager>.GetAsync();
if (pluginManager.IsManifestBanned(this.Manifest))
throw new BannedPluginException($"Unable to load {this.Name}, banned");
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
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}");
if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", 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("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
if (this.Manifest.LoadRequiredState == 0)
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();
await this.pluginLoadStateLock.WaitAsync();
try
{
switch (this.State)
{
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");
case PluginState.Unloaded:
break;
case PluginState.Loading:
case PluginState.Unloading:
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (pluginManager.IsManifestBanned(this.Manifest))
throw new BannedPluginException($"Unable to load {this.Name}, banned");
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
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.Loading;
Log.Information($"Loading {this.DllFile.Name}");
if (this.DllFile.DirectoryName != null &&
File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error(
"==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====",
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("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error(
"Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
if (reloading || this.IsDev)
@ -309,7 +347,8 @@ internal class LocalPlugin : IDisposable
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)));
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;
@ -319,7 +358,8 @@ internal class LocalPlugin : IDisposable
if (otherPlugin == this || otherPlugin.instance == null)
continue;
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
var otherPluginAssemblyName =
otherPlugin.instance.GetType().Assembly.GetName().Name;
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{
this.State = PluginState.Unloaded;
@ -330,17 +370,29 @@ internal class LocalPlugin : IDisposable
}
// Update the location for the Location and CodeBase patches
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] =
new PluginPatchData(this.DllFile);
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
this.DalamudInterface =
new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1)
{
this.instance = await framework.RunOnFrameworkThread(
() => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin;
}
else
{
this.instance =
await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
}
var ioc = Service<ServiceContainer>.Get();
this.instance = await ioc.CreateAsync(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;
this.DalamudInterface.ExplicitDispose();
Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
Log.Error(
$"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return;
}
@ -363,6 +415,10 @@ internal class LocalPlugin : IDisposable
throw;
}
finally
{
this.pluginLoadStateLock.Release();
}
}
/// <summary>
@ -370,31 +426,40 @@ internal class LocalPlugin : IDisposable
/// in the plugin list until it has been actually disposed.
/// </summary>
/// <param name="reloading">Unload while reloading.</param>
public void Unload(bool reloading = false)
/// <param name="waitBeforeLoaderDispose">Wait before disposing loader.</param>
/// <returns>The task.</returns>
public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true)
{
// Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
switch (this.State)
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
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");
case PluginState.Loaded:
break;
case PluginState.LoadError:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
var configuration = Service<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();
await this.pluginLoadStateLock.WaitAsync();
try
{
this.State = PluginState.InProgress;
switch (this.State)
{
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");
case PluginState.Loaded:
case PluginState.LoadError:
break;
case PluginState.Loading:
case PluginState.Unloading:
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
this.State = PluginState.Unloading;
Log.Information($"Unloading {this.DllFile.Name}");
this.instance?.Dispose();
if (this.Manifest.CanUnloadAsync || framework == null)
this.instance?.Dispose();
else
await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
@ -405,6 +470,8 @@ internal class LocalPlugin : IDisposable
if (!reloading)
{
if (waitBeforeLoaderDispose && this.loader != null)
await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
this.loader = null;
}
@ -419,6 +486,13 @@ internal class LocalPlugin : IDisposable
throw;
}
finally
{
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
Service<DtrBar>.GetNullable()?.HandleRemovedNodes();
this.pluginLoadStateLock.Release();
}
}
/// <summary>
@ -427,12 +501,7 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns>
public async Task ReloadAsync()
{
this.Unload(true);
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
var dtr = Service<DtrBar>.Get();
dtr.HandleRemovedNodes();
await this.UnloadAsync(true);
await this.LoadAsync(PluginLoadReason.Reload, true);
}
@ -444,7 +513,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
@ -471,7 +541,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");

View file

@ -156,6 +156,12 @@ internal record PluginManifest
[JsonProperty]
public int LoadPriority { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
[JsonProperty]
public bool CanUnloadAsync { get; init; }
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>

View file

@ -16,9 +16,9 @@ internal enum PluginState
UnloadError,
/// <summary>
/// Currently loading.
/// Currently unloading.
/// </summary>
InProgress,
Unloading,
/// <summary>
/// Load is successful.
@ -29,4 +29,9 @@ internal enum PluginState
/// Plugin has thrown an error during loading.
/// </summary>
LoadError,
/// <summary>
/// Currently loading.
/// </summary>
Loading,
}