diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 04e7fea07..4d328d78c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -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); diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 4fc299f6e..5ce8bc7d0 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -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; /// /// Container enabling the creation of scoped services. /// -internal interface IServiceScope : IDisposable +internal interface IServiceScope : IAsyncDisposable { /// /// Register objects that may be injected to scoped services, @@ -47,21 +49,57 @@ internal class ServiceScopeImpl : IServiceScope private readonly List privateScopedObjects = []; private readonly ConcurrentDictionary> scopeCreatedObjects = new(); + private readonly ReaderWriterLockSlim disposeLock = new(LockRecursionPolicy.SupportsRecursion); + private bool disposed; + /// Initializes a new instance of the class. /// The container this scope will use to create services. public ServiceScopeImpl(ServiceContainer container) => this.container = container; /// - 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(); + } + } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) => - this.container.CreateAsync(objectType, scopedObjects, this); + public Task 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(); + } + } /// - 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(); + } + } /// /// Create a service scoped to this scope, with private scoped objects. @@ -69,39 +107,73 @@ internal class ServiceScopeImpl : IServiceScope /// The type of object to create. /// Additional scoped objects. /// The created object, or null. - public Task 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)); - - /// - public void Dispose() + public Task 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(); } } + + /// + public async ValueTask DisposeAsync() + { + this.disposeLock.EnterWriteLock(); + this.disposed = true; + this.disposeLock.ExitWriteLock(); + + List? 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.GetNullable()?.DelayTicks(1) ?? Task.CompletedTask).SuppressException(); + + if (exceptions is not null) + throw new AggregateException(exceptions); + } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 910472f5f..b42b51525 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -375,54 +375,39 @@ internal class PluginManager : IInternalDisposableService /// 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 diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 5a3552199..581bfd724 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -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. /// -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 DismissedValidationProblems => this.devSettings.DismissedValidationProblems; /// - 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(); } /// diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index b94c40918..34641fe4b 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -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. /// -internal class LocalPlugin : IDisposable +internal class LocalPlugin : IAsyncDisposable { /// /// 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; /// /// Initializes a new instance of the class. @@ -107,7 +111,7 @@ internal class LocalPlugin : IDisposable /// /// Gets the associated with this plugin. /// - public DalamudPluginInterface? DalamudInterface { get; private set; } + public DalamudPluginInterface? DalamudInterface => this.dalamudInterface; /// /// Gets the path to the plugin DLL. @@ -220,40 +224,11 @@ internal class LocalPlugin : IDisposable /// /// Gets the service scope for this plugin. /// - public IServiceScope? ServiceScope { get; private set; } + public IServiceScope? ServiceScope => this.serviceScope; /// - public void Dispose() - { - var framework = Service.GetNullable(); - var configuration = Service.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); /// /// Load this plugin. @@ -263,7 +238,6 @@ internal class LocalPlugin : IDisposable /// A task. public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) { - var framework = await Service.GetAsync(); var ioc = await Service.GetAsync(); var pluginManager = await Service.GetAsync(); var dalamud = await Service.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 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. /// - /// Unload while reloading. - /// Wait before disposing loader. + /// How to dispose loader. /// The task. - public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true) + public async Task UnloadAsync(PluginLoaderDisposalMode disposalMode = PluginLoaderDisposalMode.WaitBeforeDispose) { - var configuration = Service.Get(); - var framework = Service.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 { } + /// Creates a new instance of the plugin. + /// Plugin manifest. + /// Service scope. + /// Type of the plugin main class. + /// Instance of . + /// A new instance of the plugin. + private static async Task CreatePluginInstance( + LocalPluginManifest manifest, + IServiceScope scope, + Type type, + DalamudPluginInterface dalamudInterface) + { + var framework = await Service.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 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() + /// Clears and disposes all resources associated with the plugin instance. + /// Whether to clear and dispose . + /// Exceptions, if any occurred. + private async Task ClearAndDisposeAllResources(PluginLoaderDisposalMode disposalMode) { - if (this.instance != null) - throw new InvalidOperationException("Plugin instance should be disposed at this point"); + List? 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.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.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 AttemptCleanup( + string name, + T? what, + TContext context, + Func 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; + } } } diff --git a/Dalamud/Plugin/Internal/Types/PluginLoaderDisposalMode.cs b/Dalamud/Plugin/Internal/Types/PluginLoaderDisposalMode.cs new file mode 100644 index 000000000..495205089 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginLoaderDisposalMode.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +using Dalamud.Plugin.Internal.Loader; + +namespace Dalamud.Plugin.Internal.Types; + +/// Specify how to dispose . +internal enum PluginLoaderDisposalMode +{ + /// Do not dispose the plugin loader. + None, + + /// Whether to wait a few before disposing the loader, just in case there are s + /// from the plugin that are still running. + WaitBeforeDispose, + + /// Immediately dispose the plugin loader. + ImmediateDispose, +} diff --git a/Dalamud/Utility/TaskExtensions.cs b/Dalamud/Utility/TaskExtensions.cs index a65956325..2855511a1 100644 --- a/Dalamud/Utility/TaskExtensions.cs +++ b/Dalamud/Utility/TaskExtensions.cs @@ -63,6 +63,21 @@ public static class TaskExtensions #pragma warning restore RS0030 } + /// Ignores any exceptions thrown from the task. + /// Task to ignore exceptions. + /// A task that completes when completes in any state. + 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.