Make ServiceScope IAsyncDisposable

ServiceScope.Dispose was not waiting for scoped services to complete
disposing. This had an effect of letting a new plugin instance register
a DtrBar entry before previous plugin instance's entry got unregistered.

This change also cleans up unloading procedure in LocalPlugin.
This commit is contained in:
Soreepeong 2024-08-18 07:58:45 +09:00
parent fdfdee1fcb
commit 0a8f9b73fb
7 changed files with 375 additions and 184 deletions

View file

@ -114,10 +114,42 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
{ {
if (existingEntry.Title == title) if (existingEntry.Title == title)
{ {
if (existingEntry.ShouldBeRemoved)
{
if (plugin == existingEntry.OwnerPlugin)
{
Log.Debug(
"Reviving entry: {what}; owner: {plugin}({pluginId})",
title,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
}
else
{
Log.Debug(
"Reviving entry: {what}; old owner: {old}({oldId}); new owner: {new}({newId})",
title,
existingEntry.OwnerPlugin?.InternalName,
existingEntry.OwnerPlugin?.EffectiveWorkingPluginId,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
existingEntry.OwnerPlugin = plugin;
}
existingEntry.ShouldBeRemoved = false; existingEntry.ShouldBeRemoved = false;
}
this.entriesLock.ExitUpgradeableReadLock(); this.entriesLock.ExitUpgradeableReadLock();
if (plugin == existingEntry.OwnerPlugin) if (plugin == existingEntry.OwnerPlugin)
return existingEntry; return existingEntry;
Log.Debug(
"Entry already has a different owner: {what}; owner: {old}({oldId}); requester: {new}({newId})",
title,
existingEntry.OwnerPlugin?.InternalName,
existingEntry.OwnerPlugin?.EffectiveWorkingPluginId,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
throw new ArgumentException("An entry with the same title already exists."); throw new ArgumentException("An entry with the same title already exists.");
} }
} }
@ -125,6 +157,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.entriesLock.EnterWriteLock(); this.entriesLock.EnterWriteLock();
var entry = new DtrBarEntry(this.configuration, title, null) { Text = text, OwnerPlugin = plugin }; var entry = new DtrBarEntry(this.configuration, title, null) { Text = text, OwnerPlugin = plugin };
this.entries.Add(entry); this.entries.Add(entry);
Log.Debug("Adding entry: {what}; owner: {owner}", title, plugin);
// Add the entry to the end of the order list, if it's not there already. // Add the entry to the end of the order list, if it's not there already.
var dtrOrder = this.configuration.DtrOrder ??= []; var dtrOrder = this.configuration.DtrOrder ??= [];
@ -159,14 +192,23 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
{ {
if (!entry.Added) if (!entry.Added)
{ {
Log.Debug("Removing entry immediately because it is not added yet: {what}", entry.Title);
this.entriesLock.EnterWriteLock(); this.entriesLock.EnterWriteLock();
this.RemoveEntry(entry); this.RemoveEntry(entry);
this.entries.Remove(entry); this.entries.Remove(entry);
this.entriesReadOnlyCopy = null; this.entriesReadOnlyCopy = null;
this.entriesLock.ExitWriteLock(); this.entriesLock.ExitWriteLock();
} }
else if (!entry.ShouldBeRemoved)
{
Log.Debug("Queueing entry for removal: {what}", entry.Title);
entry.Remove(); entry.Remove();
}
else
{
Log.Debug("Entry is already marked for removal: {what}", entry.Title);
}
break; break;
} }
} }
@ -313,6 +355,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var data = this.entries[i]; var data = this.entries[i];
if (data.ShouldBeRemoved) if (data.ShouldBeRemoved)
{ {
Log.Debug("Removing entry from Framework.Update: {what}", data.Title);
this.entriesLock.EnterWriteLock(); this.entriesLock.EnterWriteLock();
this.entries.RemoveAt(i); this.entries.RemoveAt(i);
this.RemoveEntry(data); this.RemoveEntry(data);

View file

@ -1,16 +1,18 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Serilog; using Dalamud.Game;
using Dalamud.Utility;
namespace Dalamud.IoC.Internal; namespace Dalamud.IoC.Internal;
/// <summary> /// <summary>
/// Container enabling the creation of scoped services. /// Container enabling the creation of scoped services.
/// </summary> /// </summary>
internal interface IServiceScope : IDisposable internal interface IServiceScope : IAsyncDisposable
{ {
/// <summary> /// <summary>
/// Register objects that may be injected to scoped services, /// Register objects that may be injected to scoped services,
@ -47,21 +49,57 @@ internal class ServiceScopeImpl : IServiceScope
private readonly List<object> privateScopedObjects = []; private readonly List<object> privateScopedObjects = [];
private readonly ConcurrentDictionary<Type, Task<object>> scopeCreatedObjects = new(); private readonly ConcurrentDictionary<Type, Task<object>> scopeCreatedObjects = new();
private readonly ReaderWriterLockSlim disposeLock = new(LockRecursionPolicy.SupportsRecursion);
private bool disposed;
/// <summary>Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.</summary> /// <summary>Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.</summary>
/// <param name="container">The container this scope will use to create services.</param> /// <param name="container">The container this scope will use to create services.</param>
public ServiceScopeImpl(ServiceContainer container) => this.container = container; public ServiceScopeImpl(ServiceContainer container) => this.container = container;
/// <inheritdoc/> /// <inheritdoc/>
public void RegisterPrivateScopes(params object[] scopes) => public void RegisterPrivateScopes(params object[] scopes)
{
this.disposeLock.EnterReadLock();
try
{
ObjectDisposedException.ThrowIf(this.disposed, this);
this.privateScopedObjects.AddRange(scopes); this.privateScopedObjects.AddRange(scopes);
}
finally
{
this.disposeLock.ExitReadLock();
}
}
/// <inheritdoc /> /// <inheritdoc />
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) => public Task<object> CreateAsync(Type objectType, params object[] scopedObjects)
this.container.CreateAsync(objectType, scopedObjects, this); {
this.disposeLock.EnterReadLock();
try
{
ObjectDisposedException.ThrowIf(this.disposed, this);
return this.container.CreateAsync(objectType, scopedObjects, this);
}
finally
{
this.disposeLock.ExitReadLock();
}
}
/// <inheritdoc /> /// <inheritdoc />
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) => public Task InjectPropertiesAsync(object instance, params object[] scopedObjects)
this.container.InjectProperties(instance, scopedObjects, this); {
this.disposeLock.EnterReadLock();
try
{
ObjectDisposedException.ThrowIf(this.disposed, this);
return this.container.InjectProperties(instance, scopedObjects, this);
}
finally
{
this.disposeLock.ExitReadLock();
}
}
/// <summary> /// <summary>
/// Create a service scoped to this scope, with private scoped objects. /// Create a service scoped to this scope, with private scoped objects.
@ -69,39 +107,73 @@ internal class ServiceScopeImpl : IServiceScope
/// <param name="objectType">The type of object to create.</param> /// <param name="objectType">The type of object to create.</param>
/// <param name="scopedObjects">Additional scoped objects.</param> /// <param name="scopedObjects">Additional scoped objects.</param>
/// <returns>The created object, or null.</returns> /// <returns>The created object, or null.</returns>
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) => public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects)
this.scopeCreatedObjects.GetOrAdd( {
this.disposeLock.EnterReadLock();
try
{
ObjectDisposedException.ThrowIf(this.disposed, this);
return this.scopeCreatedObjects.GetOrAdd(
objectType, objectType,
static (objectType, p) => p.Scope.container.CreateAsync( static (objectType, p) => p.Scope.container.CreateAsync(
objectType, objectType,
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
(Scope: this, Objects: scopedObjects)); (Scope: this, Objects: scopedObjects));
}
/// <inheritdoc /> finally
public void Dispose()
{ {
foreach (var objectTask in this.scopeCreatedObjects) this.disposeLock.ExitReadLock();
{ }
objectTask.Value.ContinueWith(
static r =>
{
if (!r.IsCompletedSuccessfully)
{
if (r.Exception is { } e)
Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl));
return;
} }
switch (r.Result) /// <inheritdoc />
public async ValueTask DisposeAsync()
{
this.disposeLock.EnterWriteLock();
this.disposed = true;
this.disposeLock.ExitWriteLock();
List<Exception>? exceptions = null;
while (!this.scopeCreatedObjects.IsEmpty)
{
try
{
await Task.WhenAll(
this.scopeCreatedObjects.Keys.Select(
async type =>
{
if (!this.scopeCreatedObjects.Remove(type, out var serviceTask))
return;
switch (await serviceTask)
{ {
case IInternalDisposableService d: case IInternalDisposableService d:
d.DisposeService(); d.DisposeService();
break; break;
case IAsyncDisposable d:
await d.DisposeAsync();
break;
case IDisposable d: case IDisposable d:
d.Dispose(); d.Dispose();
break; break;
} }
}); }));
}
catch (AggregateException ae)
{
exceptions ??= [];
exceptions.AddRange(ae.Flatten().InnerExceptions);
} }
} }
// Unless Dalamud is unloading (plugin cannot be reloading at that point), ensure that there are no more
// event callback call in progress when this function returns. Since above service dispose operations should
// have unregistered the event listeners, on next framework tick, none can be running anymore.
// This has an additional effect of ensuring that DtrBar entries are completely removed on return.
// Note that this still does not handle Framework.RunOnTick with specified delays.
await (Service<Framework>.GetNullable()?.DelayTicks(1) ?? Task.CompletedTask).SuppressException();
if (exceptions is not null)
throw new AggregateException(exceptions);
}
} }

View file

@ -375,54 +375,39 @@ internal class PluginManager : IInternalDisposableService
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
var disposablePlugins = DisposeAsync(
this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); this.installedPluginsList
if (disposablePlugins.Any()) .Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError)
{ .ToArray(),
// Unload them first, just in case some of plugin codes are still running via callbacks initiated externally. this.configuration).Wait();
foreach (var plugin in disposablePlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync)) return;
{
try
{
plugin.UnloadAsync(true, false).Wait();
}
catch (Exception ex)
{
Log.Error(ex, $"Error unloading {plugin.Name}");
}
}
Task.WaitAll(disposablePlugins static async Task DisposeAsync(LocalPlugin[] disposablePlugins, DalamudConfiguration configuration)
.Where(plugin => plugin.Manifest.CanUnloadAsync)
.Select(plugin => Task.Run(async () =>
{ {
try if (disposablePlugins.Length == 0)
{ return;
await plugin.UnloadAsync(true, false);
} // Any unload/dispose operation called from this function log errors on their own.
catch (Exception ex) // Ignore all errors.
{
Log.Error(ex, $"Error unloading {plugin.Name}"); // Unload plugins that requires to be unloaded synchronously,
} // just in case some plugin codes are still running via callbacks initiated externally.
})).ToArray()); foreach (var plugin in disposablePlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
await plugin.UnloadAsync(PluginLoaderDisposalMode.None).SuppressException();
// Unload plugins that can be unloaded from any thread.
await Task.WhenAll(disposablePlugins.Select(plugin => plugin.UnloadAsync(PluginLoaderDisposalMode.None)))
.SuppressException();
// Just in case plugins still have tasks running that they didn't cancel when they should have, // Just in case plugins still have tasks running that they didn't cancel when they should have,
// give them some time to complete it. // give them some time to complete it.
Thread.Sleep(this.configuration.PluginWaitBeforeFree ?? PluginWaitBeforeFreeDefault); // This helps avoid plugins being reloaded from conflicting with itself of previous instance.
await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginWaitBeforeFreeDefault);
// Now that we've waited enough, dispose the whole plugin. // Now that we've waited enough, dispose the whole plugin.
// Since plugins should have been unloaded above, this should be done quickly. // Since plugins should have been unloaded above, this should complete quickly.
foreach (var plugin in disposablePlugins) await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask()))
{ .SuppressException();
try
{
plugin.Dispose();
}
catch (Exception e)
{
Log.Error(e, $"Error disposing {plugin.Name}");
}
}
} }
// NET8 CHORE // NET8 CHORE

View file

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,7 +15,7 @@ namespace Dalamud.Plugin.Internal.Types;
/// This class represents a dev plugin and all facets of its lifecycle. /// This class represents a dev plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc. /// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary> /// </summary>
internal class LocalDevPlugin : LocalPlugin, IDisposable internal class LocalDevPlugin : LocalPlugin
{ {
private static readonly ModuleLog Log = new("PLUGIN"); private static readonly ModuleLog Log = new("PLUGIN");
@ -101,7 +100,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
public List<string> DismissedValidationProblems => this.devSettings.DismissedValidationProblems; public List<string> DismissedValidationProblems => this.devSettings.DismissedValidationProblems;
/// <inheritdoc/> /// <inheritdoc/>
public new void Dispose() public override ValueTask DisposeAsync()
{ {
if (this.fileWatcher != null) if (this.fileWatcher != null)
{ {
@ -110,7 +109,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
this.fileWatcher.Dispose(); this.fileWatcher.Dispose();
} }
base.Dispose(); return base.DisposeAsync();
} }
/// <summary> /// <summary>

View file

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,7 +22,7 @@ namespace Dalamud.Plugin.Internal.Types;
/// This class represents a plugin and all facets of its lifecycle. /// This class represents a plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc. /// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary> /// </summary>
internal class LocalPlugin : IDisposable internal class LocalPlugin : IAsyncDisposable
{ {
/// <summary> /// <summary>
/// The underlying manifest for this plugin. /// The underlying manifest for this plugin.
@ -41,6 +43,8 @@ internal class LocalPlugin : IDisposable
private Assembly? pluginAssembly; private Assembly? pluginAssembly;
private Type? pluginType; private Type? pluginType;
private IDalamudPlugin? instance; private IDalamudPlugin? instance;
private IServiceScope? serviceScope;
private DalamudPluginInterface? dalamudInterface;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class. /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
@ -107,7 +111,7 @@ internal class LocalPlugin : IDisposable
/// <summary> /// <summary>
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin. /// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
/// </summary> /// </summary>
public DalamudPluginInterface? DalamudInterface { get; private set; } public DalamudPluginInterface? DalamudInterface => this.dalamudInterface;
/// <summary> /// <summary>
/// Gets the path to the plugin DLL. /// Gets the path to the plugin DLL.
@ -220,40 +224,11 @@ internal class LocalPlugin : IDisposable
/// <summary> /// <summary>
/// Gets the service scope for this plugin. /// Gets the service scope for this plugin.
/// </summary> /// </summary>
public IServiceScope? ServiceScope { get; private set; } public IServiceScope? ServiceScope => this.serviceScope;
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public virtual async ValueTask DisposeAsync() =>
{ await this.ClearAndDisposeAllResources(PluginLoaderDisposalMode.ImmediateDispose);
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?.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();
}
/// <summary> /// <summary>
/// Load this plugin. /// Load this plugin.
@ -263,7 +238,6 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns> /// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{ {
var framework = await Service<Framework>.GetAsync();
var ioc = await Service<ServiceContainer>.GetAsync(); var ioc = await Service<ServiceContainer>.GetAsync();
var pluginManager = await Service<PluginManager>.GetAsync(); var pluginManager = await Service<PluginManager>.GetAsync();
var dalamud = await Service<Dalamud>.GetAsync(); var dalamud = await Service<Dalamud>.GetAsync();
@ -305,6 +279,12 @@ internal class LocalPlugin : IDisposable
break; break;
case PluginState.Unloaded: case PluginState.Unloaded:
if (this.instance is not null)
{
throw new InvalidPluginOperationException(
"Plugin should have been unloaded but instance is not cleared");
}
break; break;
case PluginState.Loading: case PluginState.Loading:
case PluginState.Unloading: case PluginState.Unloading:
@ -406,39 +386,30 @@ internal class LocalPlugin : IDisposable
// NET8 CHORE // NET8 CHORE
// PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); // PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
this.DalamudInterface = this.dalamudInterface = new(this, reason);
new DalamudPluginInterface(this, reason);
this.ServiceScope = ioc.GetScope(); this.serviceScope = ioc.GetScope();
this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it this.serviceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it
try try
{ {
var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1; this.instance = await CreatePluginInstance(
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); this.manifest,
this.instance = await newInstanceTask.ConfigureAwait(false); this.serviceScope,
this.pluginType,
async Task<IDalamudPlugin> Create() => this.dalamudInterface);
(IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!); this.State = PluginState.Loaded;
Log.Information("Finished loading {PluginName}", this.InternalName);
} }
catch (Exception ex) catch (Exception ex)
{
Log.Error(ex, "Exception during plugin initialization");
this.instance = null;
}
if (this.instance == null)
{ {
this.State = PluginState.LoadError; this.State = PluginState.LoadError;
this.UnloadAndDisposeState();
Log.Error( Log.Error(
"Error while loading {PluginName}, failed to bind and call the plugin constructor", this.InternalName); ex,
return; "Error while loading {PluginName}, failed to bind and call the plugin constructor",
this.InternalName);
await this.ClearAndDisposeAllResources(PluginLoaderDisposalMode.ImmediateDispose);
} }
this.State = PluginState.Loaded;
Log.Information("Finished loading {PluginName}", this.InternalName);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -462,14 +433,10 @@ internal class LocalPlugin : IDisposable
/// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay /// 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. /// in the plugin list until it has been actually disposed.
/// </summary> /// </summary>
/// <param name="reloading">Unload while reloading.</param> /// <param name="disposalMode">How to dispose loader.</param>
/// <param name="waitBeforeLoaderDispose">Wait before disposing loader.</param>
/// <returns>The task.</returns> /// <returns>The task.</returns>
public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true) public async Task UnloadAsync(PluginLoaderDisposalMode disposalMode = PluginLoaderDisposalMode.WaitBeforeDispose)
{ {
var configuration = Service<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();
await this.pluginLoadStateLock.WaitAsync(); await this.pluginLoadStateLock.WaitAsync();
try try
{ {
@ -498,31 +465,10 @@ internal class LocalPlugin : IDisposable
this.State = PluginState.Unloading; this.State = PluginState.Unloading;
Log.Information("Unloading {PluginName}", this.InternalName); Log.Information("Unloading {PluginName}", this.InternalName);
try if (await this.ClearAndDisposeAllResources(disposalMode) is { } ex)
{
if (this.manifest.CanUnloadAsync || framework == null)
this.instance?.Dispose();
else
await framework.RunOnFrameworkThread(() => this.instance?.Dispose()).ConfigureAwait(false);
}
catch (Exception e)
{ {
this.State = PluginState.UnloadError; this.State = PluginState.UnloadError;
Log.Error(e, "Could not unload {PluginName}, error in plugin dispose", this.InternalName); throw ex;
return;
}
finally
{
this.instance = null;
this.UnloadAndDisposeState();
if (!reloading)
{
if (waitBeforeLoaderDispose && this.loader != null)
await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
this.loader = null;
}
} }
this.State = PluginState.Unloaded; this.State = PluginState.Unloaded;
@ -549,7 +495,7 @@ internal class LocalPlugin : IDisposable
{ {
// Don't unload if we're a dev plugin and have an unload error, this is a bad idea but whatever // Don't unload if we're a dev plugin and have an unload error, this is a bad idea but whatever
if (this.IsDev && this.State != PluginState.UnloadError) if (this.IsDev && this.State != PluginState.UnloadError)
await this.UnloadAsync(true); await this.UnloadAsync(PluginLoaderDisposalMode.None);
await this.LoadAsync(PluginLoadReason.Reload, true); await this.LoadAsync(PluginLoadReason.Reload, true);
} }
@ -617,6 +563,26 @@ internal class LocalPlugin : IDisposable
{ {
} }
/// <summary>Creates a new instance of the plugin.</summary>
/// <param name="manifest">Plugin manifest.</param>
/// <param name="scope">Service scope.</param>
/// <param name="type">Type of the plugin main class.</param>
/// <param name="dalamudInterface">Instance of <see cref="IDalamudPluginInterface"/>.</param>
/// <returns>A new instance of the plugin.</returns>
private static async Task<IDalamudPlugin> CreatePluginInstance(
LocalPluginManifest manifest,
IServiceScope scope,
Type type,
DalamudPluginInterface dalamudInterface)
{
var framework = await Service<Framework>.GetAsync();
var forceFrameworkThread = manifest.LoadSync && manifest.LoadRequiredState is 0 or 1;
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
return await newInstanceTask.ConfigureAwait(false);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface);
}
private static void SetupLoaderConfig(LoaderConfig config) private static void SetupLoaderConfig(LoaderConfig config)
{ {
config.IsUnloadable = true; config.IsUnloadable = true;
@ -688,18 +654,110 @@ internal class LocalPlugin : IDisposable
} }
} }
private void UnloadAndDisposeState() /// <summary>Clears and disposes all resources associated with the plugin instance.</summary>
/// <param name="disposalMode">Whether to clear and dispose <see cref="loader"/>.</param>
/// <returns>Exceptions, if any occurred.</returns>
private async Task<AggregateException?> ClearAndDisposeAllResources(PluginLoaderDisposalMode disposalMode)
{ {
if (this.instance != null) List<Exception>? exceptions = null;
throw new InvalidOperationException("Plugin instance should be disposed at this point"); Log.Verbose(
"{name}({id}): {fn}(disposalMode={disposalMode})",
this.InternalName,
this.EffectiveWorkingPluginId,
nameof(this.ClearAndDisposeAllResources),
disposalMode);
this.DalamudInterface?.Dispose(); // Clear the plugin instance first.
this.DalamudInterface = null; if (!await AttemptCleanup(
nameof(this.instance),
this.ServiceScope?.Dispose(); Interlocked.Exchange(ref this.instance, null),
this.ServiceScope = null; this.manifest,
static async (inst, manifest) =>
{
var framework = Service<Framework>.GetNullable();
if (manifest.CanUnloadAsync || framework is null)
inst.Dispose();
else
await framework.RunOnFrameworkThread(inst.Dispose).ConfigureAwait(false);
}))
{
// Plugin was not loaded; loader is not referenced anyway, so no need to wait.
disposalMode = PluginLoaderDisposalMode.ImmediateDispose;
}
// Fields below are expected to be alive until the plugin is (attempted) disposed.
// Clear them after this point.
this.pluginType = null; this.pluginType = null;
this.pluginAssembly = null; this.pluginAssembly = null;
await AttemptCleanup(
nameof(this.serviceScope),
Interlocked.Exchange(ref this.serviceScope, null),
0,
static (x, _) => x.DisposeAsync());
await AttemptCleanup(
nameof(this.dalamudInterface),
Interlocked.Exchange(ref this.dalamudInterface, null),
0,
static (x, _) =>
{
x.Dispose();
return ValueTask.CompletedTask;
});
if (disposalMode != PluginLoaderDisposalMode.None)
{
await AttemptCleanup(
nameof(this.loader),
Interlocked.Exchange(ref this.loader, null),
disposalMode == PluginLoaderDisposalMode.WaitBeforeDispose
? Service<DalamudConfiguration>.Get().PluginWaitBeforeFree ??
PluginManager.PluginWaitBeforeFreeDefault
: 0,
static async (ldr, waitBeforeDispose) =>
{
// Just in case plugins still have tasks running that they didn't cancel when they should have,
// give them some time to complete it.
// This helps avoid plugins being reloaded from conflicting with itself of previous instance.
await Task.Delay(waitBeforeDispose);
ldr.Dispose();
});
}
return exceptions is not null
? (AggregateException)ExceptionDispatchInfo.SetCurrentStackTrace(new AggregateException(exceptions))
: null;
async ValueTask<bool> AttemptCleanup<T, TContext>(
string name,
T? what,
TContext context,
Func<T, TContext, ValueTask> cb)
where T : class
{
if (what is null)
return false;
try
{
await cb.Invoke(what, context);
Log.Verbose("{name}({id}): {what} disposed", this.InternalName, this.EffectiveWorkingPluginId, name);
}
catch (Exception ex)
{
exceptions ??= [];
exceptions.Add(ex);
Log.Error(
ex,
"{name}({id}): Failed to dispose {what}",
this.InternalName,
this.EffectiveWorkingPluginId,
name);
}
return true;
}
} }
} }

View file

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Dalamud.Plugin.Internal.Loader;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>Specify how to dispose <see cref="PluginLoader"/>.</summary>
internal enum PluginLoaderDisposalMode
{
/// <summary>Do not dispose the plugin loader.</summary>
None,
/// <summary>Whether to wait a few before disposing the loader, just in case there are <see cref="Task{TResult}"/>s
/// from the plugin that are still running.</summary>
WaitBeforeDispose,
/// <summary>Immediately dispose the plugin loader.</summary>
ImmediateDispose,
}

View file

@ -63,6 +63,21 @@ public static class TaskExtensions
#pragma warning restore RS0030 #pragma warning restore RS0030
} }
/// <summary>Ignores any exceptions thrown from the task.</summary>
/// <param name="task">Task to ignore exceptions.</param>
/// <returns>A task that completes when <paramref name="task"/> completes in any state.</returns>
public static async Task SuppressException(this Task task)
{
try
{
await task;
}
catch
{
// ignore
}
}
private static bool IsWaitingValid(Task task) private static bool IsWaitingValid(Task task)
{ {
// In the case the task has been started with the LongRunning flag, it will not be in the TPL thread pool and we can allow waiting regardless. // In the case the task has been started with the LongRunning flag, it will not be in the TPL thread pool and we can allow waiting regardless.