mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-13 12:14:16 +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)
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
#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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue