mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
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:
parent
fdfdee1fcb
commit
0a8f9b73fb
7 changed files with 375 additions and 184 deletions
|
|
@ -114,10 +114,42 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
{
|
||||
if (existingEntry.Title == title)
|
||||
{
|
||||
existingEntry.ShouldBeRemoved = false;
|
||||
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;
|
||||
}
|
||||
|
||||
this.entriesLock.ExitUpgradeableReadLock();
|
||||
if (plugin == existingEntry.OwnerPlugin)
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +157,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
this.entriesLock.EnterWriteLock();
|
||||
var entry = new DtrBarEntry(this.configuration, title, null) { Text = text, OwnerPlugin = plugin };
|
||||
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.
|
||||
var dtrOrder = this.configuration.DtrOrder ??= [];
|
||||
|
|
@ -159,14 +192,23 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
{
|
||||
if (!entry.Added)
|
||||
{
|
||||
Log.Debug("Removing entry immediately because it is not added yet: {what}", entry.Title);
|
||||
this.entriesLock.EnterWriteLock();
|
||||
this.RemoveEntry(entry);
|
||||
this.entries.Remove(entry);
|
||||
this.entriesReadOnlyCopy = null;
|
||||
this.entriesLock.ExitWriteLock();
|
||||
}
|
||||
else if (!entry.ShouldBeRemoved)
|
||||
{
|
||||
Log.Debug("Queueing entry for removal: {what}", entry.Title);
|
||||
entry.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Entry is already marked for removal: {what}", entry.Title);
|
||||
}
|
||||
|
||||
entry.Remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -313,6 +355,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
var data = this.entries[i];
|
||||
if (data.ShouldBeRemoved)
|
||||
{
|
||||
Log.Debug("Removing entry from Framework.Update: {what}", data.Title);
|
||||
this.entriesLock.EnterWriteLock();
|
||||
this.entries.RemoveAt(i);
|
||||
this.RemoveEntry(data);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Serilog;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.IoC.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Container enabling the creation of scoped services.
|
||||
/// </summary>
|
||||
internal interface IServiceScope : IDisposable
|
||||
internal interface IServiceScope : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register objects that may be injected to scoped services,
|
||||
|
|
@ -47,21 +49,57 @@ internal class ServiceScopeImpl : IServiceScope
|
|||
private readonly List<object> privateScopedObjects = [];
|
||||
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>
|
||||
/// <param name="container">The container this scope will use to create services.</param>
|
||||
public ServiceScopeImpl(ServiceContainer container) => this.container = container;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPrivateScopes(params object[] scopes) =>
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
public void RegisterPrivateScopes(params object[] scopes)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) =>
|
||||
this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) =>
|
||||
this.container.InjectProperties(instance, scopedObjects, this);
|
||||
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects)
|
||||
{
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.container.InjectProperties(instance, scopedObjects, this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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="scopedObjects">Additional scoped objects.</param>
|
||||
/// <returns>The created object, or null.</returns>
|
||||
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) =>
|
||||
this.scopeCreatedObjects.GetOrAdd(
|
||||
objectType,
|
||||
static (objectType, p) => p.Scope.container.CreateAsync(
|
||||
objectType,
|
||||
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
|
||||
(Scope: this, Objects: scopedObjects));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
foreach (var objectTask in this.scopeCreatedObjects)
|
||||
this.disposeLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
case IInternalDisposableService d:
|
||||
d.DisposeService();
|
||||
break;
|
||||
case IDisposable d:
|
||||
d.Dispose();
|
||||
break;
|
||||
}
|
||||
});
|
||||
ObjectDisposedException.ThrowIf(this.disposed, this);
|
||||
return this.scopeCreatedObjects.GetOrAdd(
|
||||
objectType,
|
||||
static (objectType, p) => p.Scope.container.CreateAsync(
|
||||
objectType,
|
||||
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
|
||||
(Scope: this, Objects: scopedObjects));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.disposeLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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:
|
||||
d.DisposeService();
|
||||
break;
|
||||
case IAsyncDisposable d:
|
||||
await d.DisposeAsync();
|
||||
break;
|
||||
case IDisposable d:
|
||||
d.Dispose();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,54 +375,39 @@ internal class PluginManager : IInternalDisposableService
|
|||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
var disposablePlugins =
|
||||
this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray();
|
||||
if (disposablePlugins.Any())
|
||||
{
|
||||
// Unload them first, just in case some of plugin codes are still running via callbacks initiated externally.
|
||||
foreach (var plugin in disposablePlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.UnloadAsync(true, false).Wait();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error unloading {plugin.Name}");
|
||||
}
|
||||
}
|
||||
DisposeAsync(
|
||||
this.installedPluginsList
|
||||
.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError)
|
||||
.ToArray(),
|
||||
this.configuration).Wait();
|
||||
return;
|
||||
|
||||
Task.WaitAll(disposablePlugins
|
||||
.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());
|
||||
static async Task DisposeAsync(LocalPlugin[] disposablePlugins, DalamudConfiguration configuration)
|
||||
{
|
||||
if (disposablePlugins.Length == 0)
|
||||
return;
|
||||
|
||||
// Any unload/dispose operation called from this function log errors on their own.
|
||||
// Ignore all errors.
|
||||
|
||||
// Unload plugins that requires to be unloaded synchronously,
|
||||
// just in case some plugin codes are still running via callbacks initiated externally.
|
||||
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,
|
||||
// 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.
|
||||
// Since plugins should have been unloaded above, this should be done quickly.
|
||||
foreach (var plugin in disposablePlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Error disposing {plugin.Name}");
|
||||
}
|
||||
}
|
||||
// Since plugins should have been unloaded above, this should complete quickly.
|
||||
await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask()))
|
||||
.SuppressException();
|
||||
}
|
||||
|
||||
// NET8 CHORE
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
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.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
internal class LocalDevPlugin : LocalPlugin, IDisposable
|
||||
internal class LocalDevPlugin : LocalPlugin
|
||||
{
|
||||
private static readonly ModuleLog Log = new("PLUGIN");
|
||||
|
||||
|
|
@ -101,7 +100,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
|
|||
public List<string> DismissedValidationProblems => this.devSettings.DismissedValidationProblems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public new void Dispose()
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
if (this.fileWatcher != null)
|
||||
{
|
||||
|
|
@ -110,7 +109,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
|
|||
this.fileWatcher.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose();
|
||||
return base.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -20,7 +22,7 @@ namespace Dalamud.Plugin.Internal.Types;
|
|||
/// This class represents a plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
internal class LocalPlugin : IDisposable
|
||||
internal class LocalPlugin : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The underlying manifest for this plugin.
|
||||
|
|
@ -41,6 +43,8 @@ internal class LocalPlugin : IDisposable
|
|||
private Assembly? pluginAssembly;
|
||||
private Type? pluginType;
|
||||
private IDalamudPlugin? instance;
|
||||
private IServiceScope? serviceScope;
|
||||
private DalamudPluginInterface? dalamudInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
|
||||
|
|
@ -107,7 +111,7 @@ internal class LocalPlugin : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
|
||||
/// </summary>
|
||||
public DalamudPluginInterface? DalamudInterface { get; private set; }
|
||||
public DalamudPluginInterface? DalamudInterface => this.dalamudInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin DLL.
|
||||
|
|
@ -220,40 +224,11 @@ internal class LocalPlugin : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the service scope for this plugin.
|
||||
/// </summary>
|
||||
public IServiceScope? ServiceScope { get; private set; }
|
||||
public IServiceScope? ServiceScope => this.serviceScope;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
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();
|
||||
}
|
||||
public virtual async ValueTask DisposeAsync() =>
|
||||
await this.ClearAndDisposeAllResources(PluginLoaderDisposalMode.ImmediateDispose);
|
||||
|
||||
/// <summary>
|
||||
/// Load this plugin.
|
||||
|
|
@ -263,7 +238,6 @@ internal class LocalPlugin : IDisposable
|
|||
/// <returns>A task.</returns>
|
||||
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
|
||||
{
|
||||
var framework = await Service<Framework>.GetAsync();
|
||||
var ioc = await Service<ServiceContainer>.GetAsync();
|
||||
var pluginManager = await Service<PluginManager>.GetAsync();
|
||||
var dalamud = await Service<Dalamud>.GetAsync();
|
||||
|
|
@ -300,11 +274,17 @@ internal class LocalPlugin : IDisposable
|
|||
if (!this.IsDev)
|
||||
{
|
||||
throw new InvalidPluginOperationException(
|
||||
$"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
|
||||
$"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
|
||||
}
|
||||
|
||||
break;
|
||||
case PluginState.Unloaded:
|
||||
if (this.instance is not null)
|
||||
{
|
||||
throw new InvalidPluginOperationException(
|
||||
"Plugin should have been unloaded but instance is not cleared");
|
||||
}
|
||||
|
||||
break;
|
||||
case PluginState.Loading:
|
||||
case PluginState.Unloading:
|
||||
|
|
@ -406,39 +386,30 @@ internal class LocalPlugin : IDisposable
|
|||
// NET8 CHORE
|
||||
// PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
|
||||
|
||||
this.DalamudInterface =
|
||||
new DalamudPluginInterface(this, reason);
|
||||
this.dalamudInterface = new(this, reason);
|
||||
|
||||
this.ServiceScope = ioc.GetScope();
|
||||
this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it
|
||||
this.serviceScope = ioc.GetScope();
|
||||
this.serviceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it
|
||||
|
||||
try
|
||||
{
|
||||
var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1;
|
||||
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
|
||||
this.instance = await newInstanceTask.ConfigureAwait(false);
|
||||
|
||||
async Task<IDalamudPlugin> Create() =>
|
||||
(IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!);
|
||||
this.instance = await CreatePluginInstance(
|
||||
this.manifest,
|
||||
this.serviceScope,
|
||||
this.pluginType,
|
||||
this.dalamudInterface);
|
||||
this.State = PluginState.Loaded;
|
||||
Log.Information("Finished loading {PluginName}", this.InternalName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Exception during plugin initialization");
|
||||
this.instance = null;
|
||||
}
|
||||
|
||||
if (this.instance == null)
|
||||
{
|
||||
this.State = PluginState.LoadError;
|
||||
this.UnloadAndDisposeState();
|
||||
|
||||
Log.Error(
|
||||
"Error while loading {PluginName}, failed to bind and call the plugin constructor", this.InternalName);
|
||||
return;
|
||||
ex,
|
||||
"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)
|
||||
{
|
||||
|
|
@ -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
|
||||
/// in the plugin list until it has been actually disposed.
|
||||
/// </summary>
|
||||
/// <param name="reloading">Unload while reloading.</param>
|
||||
/// <param name="waitBeforeLoaderDispose">Wait before disposing loader.</param>
|
||||
/// <param name="disposalMode">How to dispose loader.</param>
|
||||
/// <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();
|
||||
try
|
||||
{
|
||||
|
|
@ -498,31 +465,10 @@ internal class LocalPlugin : IDisposable
|
|||
this.State = PluginState.Unloading;
|
||||
Log.Information("Unloading {PluginName}", this.InternalName);
|
||||
|
||||
try
|
||||
{
|
||||
if (this.manifest.CanUnloadAsync || framework == null)
|
||||
this.instance?.Dispose();
|
||||
else
|
||||
await framework.RunOnFrameworkThread(() => this.instance?.Dispose()).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
if (await this.ClearAndDisposeAllResources(disposalMode) is { } ex)
|
||||
{
|
||||
this.State = PluginState.UnloadError;
|
||||
Log.Error(e, "Could not unload {PluginName}, error in plugin dispose", this.InternalName);
|
||||
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;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
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
|
||||
if (this.IsDev && this.State != PluginState.UnloadError)
|
||||
await this.UnloadAsync(true);
|
||||
await this.UnloadAsync(PluginLoaderDisposalMode.None);
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
throw new InvalidOperationException("Plugin instance should be disposed at this point");
|
||||
List<Exception>? exceptions = null;
|
||||
Log.Verbose(
|
||||
"{name}({id}): {fn}(disposalMode={disposalMode})",
|
||||
this.InternalName,
|
||||
this.EffectiveWorkingPluginId,
|
||||
nameof(this.ClearAndDisposeAllResources),
|
||||
disposalMode);
|
||||
|
||||
this.DalamudInterface?.Dispose();
|
||||
this.DalamudInterface = null;
|
||||
|
||||
this.ServiceScope?.Dispose();
|
||||
this.ServiceScope = null;
|
||||
// Clear the plugin instance first.
|
||||
if (!await AttemptCleanup(
|
||||
nameof(this.instance),
|
||||
Interlocked.Exchange(ref this.instance, 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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
Dalamud/Plugin/Internal/Types/PluginLoaderDisposalMode.cs
Normal file
19
Dalamud/Plugin/Internal/Types/PluginLoaderDisposalMode.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -63,6 +63,21 @@ public static class TaskExtensions
|
|||
#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)
|
||||
{
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue