Add IInternal/PublicDisposableService (#1696)

* Add IInternal/PublicDisposableService

Plugins are exposed interfaces that are not inherited from
`IDisposable`, but services implementing plugin interfaces often
implement `IDisposable`. Some plugins may try to call
`IDisposable.Dispose` on everything provided, and it also is possible to
use `using` clause too eagerly while working on Dalamud itself, such as
writing `using var smth = await Service<SomeService>.GetAsync();`. Such
behaviors often lead to a difficult-to-debug errors, and making those
services either not an `IDisposable` or making `IDisposable.Dispose` do
nothing if the object has been loaded would prevent such errors. As
`ServiceManager` must be the only class dealing with construction and
disposal of services, `IInternalDisposableService` has been added to
limit who can dispose the object. `IPublicDisposableService` also has
been added to classes that can be constructed and accessed directly by
plugins; for those, `Dispose` will be ignored if the instance is a
service instance, and only `DisposeService` will respond.

In addition, `DalamudPluginInterface` and `UiBuilder` also have been
changed so that their `IDisposable.Dispose` no longer respond, and
instead, internal functions have been added to only allow disposal from
Dalamud.

* Cleanup

* Postmerge fixes

* More explanation on RunOnFrameworkThread(ClearHooks)

* Mark ReliableFileStorage public ctor obsolete

---------

Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
This commit is contained in:
srkizer 2024-03-17 00:58:05 +09:00 committed by GitHub
parent dcec076ca7
commit 87b9edb448
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 441 additions and 381 deletions

View file

@ -97,8 +97,6 @@ namespace Dalamud.CorePlugin
this.Interface.UiBuilder.Draw -= this.OnDraw;
this.windowSystem.RemoveAllWindows();
this.Interface.ExplicitDispose();
}
/// <summary>

View file

@ -26,7 +26,7 @@ namespace Dalamud.Configuration.Internal;
#pragma warning disable SA1015
[InherentDependency<ReliableFileStorage>] // We must still have this when unloading
#pragma warning restore SA1015
internal sealed class DalamudConfiguration : IServiceType, IDisposable
internal sealed class DalamudConfiguration : IInternalDisposableService
{
private static readonly JsonSerializerSettings SerializerSettings = new()
{
@ -502,7 +502,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
// Make sure that we save, if a save is queued while we are shutting down
this.Update();

View file

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
@ -187,27 +186,6 @@ internal sealed class Dalamud : IServiceType
this.unloadSignal.WaitOne();
}
/// <summary>
/// Dispose subsystems related to plugin handling.
/// </summary>
public void DisposePlugins()
{
// this must be done before unloading interface manager, in order to do rebuild
// the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game
// will not receive any windows messages
Service<DalamudIme>.GetNullable()?.Dispose();
// this must be done before unloading plugins, or it can cause a race condition
// due to rendering happening on another thread, where a plugin might receive
// a render call after it has been disposed, which can crash if it attempts to
// use any resources that it freed in its own Dispose method
Service<InterfaceManager>.GetNullable()?.Dispose();
Service<DalamudInterface>.GetNullable()?.Dispose();
Service<PluginManager>.GetNullable()?.Dispose();
}
/// <summary>
/// Replace the current exception handler with the default one.
/// </summary>

View file

@ -27,7 +27,7 @@ namespace Dalamud.Data;
#pragma warning disable SA1015
[ResolveVia<IDataManager>]
#pragma warning restore SA1015
internal sealed class DataManager : IDisposable, IServiceType, IDataManager
internal sealed class DataManager : IInternalDisposableService, IDataManager
{
private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource;
@ -158,7 +158,7 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager
#endregion
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.luminaCancellationTokenSource.Cancel();
}

View file

@ -138,7 +138,9 @@ public sealed class EntryPoint
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level
#pragma warning disable CS0618 // Type or member is obsolete
var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
#pragma warning restore CS0618 // Type or member is obsolete
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
// Set the appropriate logging level from the configuration

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Events;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType
internal unsafe class AddonEventManager : IInternalDisposableService
{
/// <summary>
/// PluginName for Dalamud Internal use.
@ -62,7 +62,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.onUpdateCursor.Dispose();
@ -204,7 +204,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
#pragma warning disable SA1015
[ResolveVia<IAddonEventManager>]
#pragma warning restore SA1015
internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager
internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager
{
[ServiceManager.ServiceDependency]
private readonly AddonEventManager eventManagerService = Service<AddonEventManager>.Get();
@ -225,7 +225,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
// if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
if (this.isForcingCursor)

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IDisposable, IServiceType
internal unsafe class AddonLifecycle : IInternalDisposableService
{
private static readonly ModuleLog Log = new("AddonLifecycle");
@ -89,7 +89,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.onAddonSetupHook.Dispose();
this.onAddonSetup2Hook.Dispose();
@ -383,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
#pragma warning disable SA1015
[ResolveVia<IAddonLifecycle>]
#pragma warning restore SA1015
internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle
internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
@ -391,7 +391,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif
private readonly List<AddonLifecycleEventListener> eventListeners = new();
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{

View file

@ -23,7 +23,7 @@ namespace Dalamud.Game.ClientState;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class ClientState : IDisposable, IServiceType, IClientState
internal sealed class ClientState : IInternalDisposableService, IClientState
{
private static readonly ModuleLog Log = new("ClientState");
@ -115,7 +115,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
@ -196,7 +196,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
#pragma warning disable SA1015
[ResolveVia<IClientState>]
#pragma warning restore SA1015
internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState
internal class ClientStatePluginScoped : IInternalDisposableService, IClientState
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientStateService = Service<ClientState>.Get();
@ -257,7 +257,7 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState
public bool IsGPosing => this.clientStateService.IsGPosing;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.Login -= this.LoginForward;

View file

@ -10,7 +10,7 @@ namespace Dalamud.Game.ClientState.Conditions;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed partial class Condition : IServiceType, ICondition
internal sealed partial class Condition : IInternalDisposableService, ICondition
{
/// <summary>
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
@ -22,6 +22,8 @@ internal sealed partial class Condition : IServiceType, ICondition
private readonly bool[] cache = new bool[MaxConditionEntries];
private bool isDisposed;
[ServiceManager.ServiceConstructor]
private Condition(ClientState clientState)
{
@ -35,6 +37,9 @@ internal sealed partial class Condition : IServiceType, ICondition
this.framework.Update += this.FrameworkUpdate;
}
/// <summary>Finalizes an instance of the <see cref="Condition" /> class.</summary>
~Condition() => this.Dispose(false);
/// <inheritdoc/>
public event ICondition.ConditionChangeDelegate? ConditionChange;
@ -60,6 +65,9 @@ internal sealed partial class Condition : IServiceType, ICondition
public bool this[ConditionFlag flag]
=> this[(int)flag];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
/// <inheritdoc/>
public bool Any()
{
@ -89,6 +97,19 @@ internal sealed partial class Condition : IServiceType, ICondition
return false;
}
private void Dispose(bool disposing)
{
if (this.isDisposed)
return;
if (disposing)
{
this.framework.Update -= this.FrameworkUpdate;
}
this.isDisposed = true;
}
private void FrameworkUpdate(IFramework unused)
{
for (var i = 0; i < MaxConditionEntries; i++)
@ -112,44 +133,6 @@ internal sealed partial class Condition : IServiceType, ICondition
}
}
/// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary>
internal sealed partial class Condition : IDisposable
{
private bool isDisposed;
/// <summary>
/// Finalizes an instance of the <see cref="Condition" /> class.
/// </summary>
~Condition()
{
this.Dispose(false);
}
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
private void Dispose(bool disposing)
{
if (this.isDisposed)
return;
if (disposing)
{
this.framework.Update -= this.FrameworkUpdate;
}
this.isDisposed = true;
}
}
/// <summary>
/// Plugin-scoped version of a Condition service.
/// </summary>
@ -159,7 +142,7 @@ internal sealed partial class Condition : IDisposable
#pragma warning disable SA1015
[ResolveVia<ICondition>]
#pragma warning restore SA1015
internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition
internal class ConditionPluginScoped : IInternalDisposableService, ICondition
{
[ServiceManager.ServiceDependency]
private readonly Condition conditionService = Service<Condition>.Get();
@ -185,7 +168,7 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition
public bool this[int flag] => this.conditionService[flag];
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.conditionService.ConditionChange -= this.ConditionChangedForward;

View file

@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad;
#pragma warning disable SA1015
[ResolveVia<IGamepadState>]
#pragma warning restore SA1015
internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
{
private readonly Hook<ControllerPoll>? gamepadPoll;
@ -109,7 +109,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.Dispose(true);
GC.SuppressFinalize(this);

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.Command;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager
internal sealed class CommandManager : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
@ -130,7 +130,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage
}
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
}
@ -170,7 +170,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage
#pragma warning disable SA1015
[ResolveVia<ICommandManager>]
#pragma warning restore SA1015
internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
@ -193,7 +193,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM
public ReadOnlyDictionary<string, CommandInfo> Commands => this.commandManagerService.Commands;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
foreach (var command in this.pluginRegisteredCommands)
{

View file

@ -15,7 +15,7 @@ namespace Dalamud.Game.Config;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
{
private readonly TaskCompletionSource tcsInitialization = new();
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
@ -195,7 +195,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
var ode = new ObjectDisposedException(nameof(GameConfig));
this.tcsInitialization.SetExceptionIfIncomplete(ode);
@ -248,7 +248,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
#pragma warning disable SA1015
[ResolveVia<IGameConfig>]
#pragma warning restore SA1015
internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
{
[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
@ -295,7 +295,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
public GameConfigSection UiControl => this.gameConfigService.UiControl;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.gameConfigService.Changed -= this.ConfigChangedForward;
this.initializationTask.ContinueWith(

View file

@ -13,7 +13,7 @@ namespace Dalamud.Game.DutyState;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
internal unsafe class DutyState : IInternalDisposableService, IDutyState
{
private readonly DutyStateAddressResolver address;
private readonly Hook<SetupContentDirectNetworkMessageDelegate> contentDirectorNetworkMessageHook;
@ -62,7 +62,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
private bool CompletedThisTerritory { get; set; }
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.contentDirectorNetworkMessageHook.Dispose();
this.framework.Update -= this.FrameworkOnUpdateEvent;
@ -168,7 +168,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
#pragma warning disable SA1015
[ResolveVia<IDutyState>]
#pragma warning restore SA1015
internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState
internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState
{
[ServiceManager.ServiceDependency]
private readonly DutyState dutyStateService = Service<DutyState>.Get();
@ -200,7 +200,7 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState
public bool IsDutyStarted => this.dutyStateService.IsDutyStarted;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.dutyStateService.DutyStarted -= this.DutyStartedForward;
this.dutyStateService.DutyWiped -= this.DutyWipedForward;

View file

@ -23,7 +23,7 @@ namespace Dalamud.Game;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class Framework : IDisposable, IServiceType, IFramework
internal sealed class Framework : IInternalDisposableService, IFramework
{
private static readonly ModuleLog Log = new("Framework");
@ -274,7 +274,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.RunOnFrameworkThread(() =>
{
@ -469,7 +469,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
#pragma warning disable SA1015
[ResolveVia<IFramework>]
#pragma warning restore SA1015
internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
{
[ServiceManager.ServiceDependency]
private readonly Framework frameworkService = Service<Framework>.Get();
@ -504,7 +504,7 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.frameworkService.Update -= this.OnUpdateForward;

View file

@ -29,7 +29,7 @@ namespace Dalamud.Game.Gui;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
private static readonly ModuleLog Log = new("ChatGui");
@ -109,7 +109,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
@ -409,7 +409,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
#pragma warning disable SA1015
[ResolveVia<IChatGui>]
#pragma warning restore SA1015
internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
{
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGuiService = Service<ChatGui>.Get();
@ -447,7 +447,7 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.chatGuiService.ChatMessage -= this.OnMessageForward;
this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;

View file

@ -28,7 +28,7 @@ namespace Dalamud.Game.Gui.ContextMenu;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu
internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu
{
private static readonly ModuleLog Log = new("ContextMenu");
@ -77,7 +77,7 @@ internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMe
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
var manager = RaptureAtkUnitManager.Instance();
var menu = manager->GetAddonByName("ContextMenu");
@ -496,7 +496,7 @@ original:
#pragma warning disable SA1015
[ResolveVia<IContextMenu>]
#pragma warning restore SA1015
internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu
{
[ServiceManager.ServiceDependency]
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
@ -514,7 +514,7 @@ internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
private object MenuItemsLock { get; } = new();
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;

View file

@ -22,7 +22,7 @@ namespace Dalamud.Game.Gui.Dtr;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
{
private const uint BaseNodeId = 1000;
@ -101,7 +101,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
}
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener);
this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener);
@ -493,7 +493,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
#pragma warning disable SA1015
[ResolveVia<IDtrBar>]
#pragma warning restore SA1015
internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar
internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
{
[ServiceManager.ServiceDependency]
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
@ -501,7 +501,7 @@ internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar
private readonly Dictionary<string, DtrBarEntry> pluginEntries = new();
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
foreach (var entry in this.pluginEntries)
{

View file

@ -16,7 +16,7 @@ namespace Dalamud.Game.Gui.FlyText;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
{
/// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
@ -78,7 +78,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.createFlyTextHook.Dispose();
}
@ -277,7 +277,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
#pragma warning disable SA1015
[ResolveVia<IFlyTextGui>]
#pragma warning restore SA1015
internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui
internal class FlyTextGuiPluginScoped : IInternalDisposableService, IFlyTextGui
{
[ServiceManager.ServiceDependency]
private readonly FlyTextGui flyTextGuiService = Service<FlyTextGui>.Get();
@ -294,7 +294,7 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui
public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward;

View file

@ -27,7 +27,7 @@ namespace Dalamud.Game.Gui;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
{
private static readonly ModuleLog Log = new("GameGui");
@ -344,7 +344,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
/// <summary>
/// Disables the hooks and submodules of this module.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
@ -520,7 +520,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
#pragma warning disable SA1015
[ResolveVia<IGameGui>]
#pragma warning restore SA1015
internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui
internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
{
[ServiceManager.ServiceDependency]
private readonly GameGui gameGuiService = Service<GameGui>.Get();
@ -558,7 +558,7 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui
public HoveredAction HoveredAction => this.gameGuiService.HoveredAction;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.gameGuiService.UiHideToggled -= this.UiHideToggledForward;
this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward;

View file

@ -15,7 +15,7 @@ namespace Dalamud.Game.Gui.PartyFinder;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui
internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui
{
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
@ -47,7 +47,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.receiveListingHook.Dispose();
@ -131,7 +131,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
#pragma warning disable SA1015
[ResolveVia<IPartyFinderGui>]
#pragma warning restore SA1015
internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui
internal class PartyFinderGuiPluginScoped : IInternalDisposableService, IPartyFinderGui
{
[ServiceManager.ServiceDependency]
private readonly PartyFinderGui partyFinderGuiService = Service<PartyFinderGui>.Get();
@ -148,7 +148,7 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin
public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward;

View file

@ -14,7 +14,7 @@ namespace Dalamud.Game.Gui.Toast;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
{
private const uint QuestToastCheckmarkMagic = 60081;
@ -73,7 +73,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.showNormalToastHook.Dispose();
this.showQuestToastHook.Dispose();
@ -383,7 +383,7 @@ internal sealed partial class ToastGui
#pragma warning disable SA1015
[ResolveVia<IToastGui>]
#pragma warning restore SA1015
internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui
internal class ToastGuiPluginScoped : IInternalDisposableService, IToastGui
{
[ServiceManager.ServiceDependency]
private readonly ToastGui toastGuiService = Service<ToastGui>.Get();
@ -408,7 +408,7 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui
public event IToastGui.OnErrorToastDelegate? ErrorToast;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.toastGuiService.Toast -= this.ToastForward;
this.toastGuiService.QuestToast -= this.QuestToastForward;

View file

@ -12,7 +12,7 @@ namespace Dalamud.Game.Internal;
/// This class disables anti-debug functionality in the game client.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed partial class AntiDebug : IServiceType
internal sealed class AntiDebug : IInternalDisposableService
{
private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 };
private byte[] original;
@ -43,16 +43,25 @@ internal sealed partial class AntiDebug : IServiceType
}
}
/// <summary>Finalizes an instance of the <see cref="AntiDebug"/> class.</summary>
~AntiDebug() => this.Disable();
/// <summary>
/// Gets a value indicating whether the anti-debugging is enabled.
/// </summary>
public bool IsEnabled { get; private set; } = false;
/// <inheritdoc />
void IInternalDisposableService.DisposeService() => this.Disable();
/// <summary>
/// Enables the anti-debugging by overwriting code in memory.
/// </summary>
public void Enable()
{
if (this.IsEnabled)
return;
this.original = new byte[this.nop.Length];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
@ -73,6 +82,9 @@ internal sealed partial class AntiDebug : IServiceType
/// </summary>
public void Disable()
{
if (!this.IsEnabled)
return;
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
@ -86,45 +98,3 @@ internal sealed partial class AntiDebug : IServiceType
this.IsEnabled = false;
}
}
/// <summary>
/// Implementing IDisposable.
/// </summary>
internal sealed partial class AntiDebug : IDisposable
{
private bool disposed = false;
/// <summary>
/// Finalizes an instance of the <see cref="AntiDebug"/> class.
/// </summary>
~AntiDebug() => this.Dispose(false);
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
/// <param name="disposing">If this was disposed through calling Dispose() or from being finalized.</param>
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
// If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded.
// If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the
// check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it.
this.Disable();
}
this.disposed = true;
}
}

View file

@ -20,7 +20,7 @@ namespace Dalamud.Game.Internal;
/// This class implements in-game Dalamud options in the in-game System menu.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
{
private readonly AtkValueChangeType atkValueChangeType;
private readonly AtkValueSetString atkValueSetString;
@ -40,6 +40,8 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
private readonly string locDalamudPlugins;
private readonly string locDalamudSettings;
private bool disposed = false;
[ServiceManager.ServiceConstructor]
private DalamudAtkTweaks(TargetSigScanner sigScanner)
{
@ -69,6 +71,9 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
}
/// <summary>Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.</summary>
~DalamudAtkTweaks() => this.Dispose(false);
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type);
@ -79,6 +84,26 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
}
this.disposed = true;
}
/*
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
{
@ -229,45 +254,3 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
}
}
}
/// <summary>
/// Implements IDisposable.
/// </summary>
internal sealed partial class DalamudAtkTweaks : IDisposable
{
private bool disposed = false;
/// <summary>
/// Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.
/// </summary>
~DalamudAtkTweaks() => this.Dispose(false);
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
}
this.disposed = true;
}
}

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.Inventory;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal class GameInventory : IDisposable, IServiceType
internal class GameInventory : IInternalDisposableService
{
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new();
private readonly List<GameInventoryPluginScoped> subscribers = new();
@ -61,7 +61,7 @@ internal class GameInventory : IDisposable, IServiceType
private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
lock (this.subscribersPendingChange)
{
@ -351,7 +351,7 @@ internal class GameInventory : IDisposable, IServiceType
#pragma warning disable SA1015
[ResolveVia<IGameInventory>]
#pragma warning restore SA1015
internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory
internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory
{
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped));
@ -406,7 +406,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven
public event IGameInventory.InventoryChangedDelegate<InventoryItemMergedArgs>? ItemMergedExplicit;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.gameInventoryService.Unsubscribe(this);

View file

@ -15,7 +15,7 @@ namespace Dalamud.Game.Network;
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<ProcessZonePacketDownDelegate> processZonePacketDownHook;
@ -59,7 +59,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.processZonePacketDownHook.Dispose();
this.processZonePacketUpHook.Dispose();
@ -145,7 +145,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
#pragma warning disable SA1015
[ResolveVia<IGameNetwork>]
#pragma warning restore SA1015
internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork
internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork
{
[ServiceManager.ServiceDependency]
private readonly GameNetwork gameNetworkService = Service<GameNetwork>.Get();
@ -162,7 +162,7 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward;

View file

@ -26,7 +26,7 @@ namespace Dalamud.Game.Network.Internal;
/// This class handles network notifications and uploading market board data.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class NetworkHandlers : IDisposable, IServiceType
internal unsafe class NetworkHandlers : IInternalDisposableService
{
private readonly IMarketBoardUploader uploader;
@ -213,7 +213,7 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.disposing = true;
this.Dispose(this.disposing);

View file

@ -10,7 +10,7 @@ namespace Dalamud.Game.Network.Internal;
/// This class enables TCP optimizations in the game socket for better performance.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class WinSockHandlers : IDisposable, IServiceType
internal sealed class WinSockHandlers : IInternalDisposableService
{
private Hook<SocketDelegate> ws2SocketHook;
@ -27,7 +27,7 @@ internal sealed class WinSockHandlers : IDisposable, IServiceType
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.ws2SocketHook?.Dispose();
}

View file

@ -104,6 +104,10 @@ public class SigScanner : IDisposable, ISigScanner
/// <inheritdoc/>
public ProcessModule Module { get; }
/// <summary>Gets or sets a value indicating whether this instance of <see cref="SigScanner"/> is meant to be a
/// Dalamud service.</summary>
private protected bool IsService { get; set; }
private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize;
/// <summary>
@ -309,13 +313,11 @@ public class SigScanner : IDisposable, ISigScanner
}
}
/// <summary>
/// Free the memory of the copied module search area on object disposal, if applicable.
/// </summary>
/// <inheritdoc/>
public void Dispose()
{
this.Save();
Marshal.FreeHGlobal(this.moduleCopyPtr);
if (!this.IsService)
this.DisposeCore();
}
/// <summary>
@ -337,6 +339,15 @@ public class SigScanner : IDisposable, ISigScanner
}
}
/// <summary>
/// Free the memory of the copied module search area on object disposal, if applicable.
/// </summary>
private protected void DisposeCore()
{
this.Save();
Marshal.FreeHGlobal(this.moduleCopyPtr);
}
/// <summary>
/// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location.
/// </summary>

View file

@ -15,7 +15,7 @@ namespace Dalamud.Game;
#pragma warning disable SA1015
[ResolveVia<ISigScanner>]
#pragma warning restore SA1015
internal class TargetSigScanner : SigScanner, IServiceType
internal class TargetSigScanner : SigScanner, IPublicDisposableService
{
/// <summary>
/// Initializes a new instance of the <see cref="TargetSigScanner"/> class.
@ -26,4 +26,14 @@ internal class TargetSigScanner : SigScanner, IServiceType
: base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)
{
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
if (this.IsService)
this.DisposeCore();
}
/// <inheritdoc/>
void IPublicDisposableService.MarkDisposeOnlyFromService() => this.IsService = true;
}

View file

@ -21,7 +21,7 @@ namespace Dalamud.Hooking.Internal;
#pragma warning disable SA1015
[ResolveVia<IGameInteropProvider>]
#pragma warning restore SA1015
internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable
internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternalDisposableService
{
private readonly LocalPlugin plugin;
private readonly SigScanner scanner;
@ -83,7 +83,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT
=> this.HookFromAddress(this.scanner.ScanText(signature), detour, backend);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray();
if (notDisposed.Length != 0)

View file

@ -14,7 +14,7 @@ namespace Dalamud.Hooking.Internal;
/// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class HookManager : IDisposable, IServiceType
internal class HookManager : IInternalDisposableService
{
/// <summary>
/// Logger shared with <see cref="Unhooker"/>.
@ -74,7 +74,7 @@ internal class HookManager : IDisposable, IServiceType
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
RevertHooks();
TrackedHooks.Clear();

View file

@ -15,7 +15,7 @@ namespace Dalamud.Hooking.WndProcHook;
/// Manages WndProc hooks for game main window and extra ImGui viewport windows.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class WndProcHookManager : IServiceType, IDisposable
internal sealed class WndProcHookManager : IInternalDisposableService
{
private static readonly ModuleLog Log = new(nameof(WndProcHookManager));
@ -56,7 +56,7 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable
public event WndProcEventDelegate? PostWndProc;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
if (this.dispatchMessageWHook.IsDisposed)
return;

View file

@ -6,3 +6,20 @@
public interface IServiceType
{
}
/// <summary><see cref="IDisposable"/>, but for <see cref="IServiceType"/>.</summary>
/// <remarks>Use this to prevent services from accidentally being disposed by plugins or <c>using</c> clauses.</remarks>
internal interface IInternalDisposableService : IServiceType
{
/// <summary>Disposes the service.</summary>
void DisposeService();
}
/// <summary>An <see cref="IInternalDisposableService"/> which happens to be public and needs to expose
/// <see cref="IDisposable.Dispose"/>.</summary>
internal interface IPublicDisposableService : IInternalDisposableService, IDisposable
{
/// <summary>Marks that only <see cref="IInternalDisposableService.DisposeService"/> should respond,
/// while suppressing <see cref="IDisposable.Dispose"/>.</summary>
void MarkDisposeOnlyFromService();
}

View file

@ -19,7 +19,7 @@ namespace Dalamud.Interface.DragDrop;
#pragma warning disable SA1015
[ResolveVia<IDragDropManager>]
#pragma warning restore SA1015
internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType
internal partial class DragDropManager : IInternalDisposableService, IDragDropManager
{
private nint windowHandlePtr = nint.Zero;
@ -56,6 +56,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService
/// <summary> Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. </summary>
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Disable();
/// <summary> Enable external drag and drop. </summary>
public void Enable()
{
@ -99,10 +102,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService
this.ServiceAvailable = false;
}
/// <inheritdoc cref="Disable"/>
public void Dispose()
=> this.Disable();
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder)
{

View file

@ -93,7 +93,7 @@ public sealed class SingleFontChooserDialog : IDisposable
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="newAsyncAtlas">A new instance of <see cref="IFontAtlas"/> created using
/// <see cref="FontAtlasAutoRebuildMode.Async"/> as its auto-rebuild mode.</param>
/// <remarks>The passed instance of <see cref="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
/// <remarks>The passed instance of <paramref see="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
/// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
/// this font chooser. Consider using <see cref="SingleFontChooserDialog(UiBuilder, bool, string?)"/> for automatic
/// handling of font atlas derived from a <see cref="UiBuilder"/>, or even <see cref="CreateAuto"/> for automatic

View file

@ -34,7 +34,7 @@ namespace Dalamud.Interface.Internal;
/// This class handles CJK IME.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudIme : IDisposable, IServiceType
internal sealed unsafe class DalamudIme : IInternalDisposableService
{
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
private const int CImGuiStbTextUndoOffset = 0xB59C0;
@ -200,7 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.interfaceManager.Draw -= this.Draw;
this.ReleaseUnmanagedResources();

View file

@ -46,7 +46,7 @@ namespace Dalamud.Interface.Internal;
/// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class DalamudInterface : IDisposable, IServiceType
internal class DalamudInterface : IInternalDisposableService
{
private const float CreditsDarkeningMaxAlpha = 0.8f;
@ -209,7 +209,7 @@ internal class DalamudInterface : IDisposable, IServiceType
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.interfaceManager.Draw -= this.OnDraw;

View file

@ -35,7 +35,7 @@ namespace Dalamud.Interface.Internal;
/// </para>
/// </remarks>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable
internal sealed unsafe class ImGuiClipboardFunctionProvider : IInternalDisposableService
{
private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider));
private readonly nint clipboardUserDataOriginal;
@ -75,7 +75,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
if (!this.clipboardUserData.IsAllocated)
return;

View file

@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal;
/// Change push_texture_id to only have one condition.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable
internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService
{
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
@ -69,7 +69,7 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl
ImDrawFlags flags);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.hookImDrawListAddPolyline.Dispose();
this.hookImDrawListAddRectFilled.Dispose();

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
@ -51,7 +52,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal class InterfaceManager : IDisposable, IServiceType
internal class InterfaceManager : IInternalDisposableService
{
/// <summary>
/// The default font size, in points.
@ -69,10 +70,13 @@ internal class InterfaceManager : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly SwapChainVtableResolver address = new();
private readonly Hook<SetCursorDelegate> setCursorHook;
private RawDX11Scene? scene;
private Hook<SetCursorDelegate>? setCursorHook;
private Hook<PresentDelegate>? presentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
@ -87,8 +91,6 @@ internal class InterfaceManager : IDisposable, IServiceType
[ServiceManager.ServiceConstructor]
private InterfaceManager()
{
this.setCursorHook = Hook<SetCursorDelegate>.FromImport(
null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -233,25 +235,45 @@ internal class InterfaceManager : IDisposable, IServiceType
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
if (Service<Framework>.GetNullable() is { } framework)
framework.RunOnFrameworkThread(Disposer).Wait();
else
Disposer();
// Unload hooks from the framework thread if possible.
// We're currently off the framework thread, as this function can only be called from
// ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread.
// The functions being unhooked are mostly called from the main thread, so unhooking from the main thread when
// possible would avoid any chance of unhooking a function that currently is being called.
// If unloading is initiated from "Unload Dalamud" /xldev menu, then the framework would still be running, as
// Framework.Destroy has never been called and thus Framework.IsFrameworkUnloading cannot be true, and this
// function will actually run the destroy from the framework thread.
// Otherwise, as Framework.IsFrameworkUnloading should have been set, this code should run immediately.
this.framework.RunOnFrameworkThread(ClearHooks).Wait();
// Below this point, hooks are guaranteed to be no longer called.
// A font resource lock outlives the parent handle and the owner atlas. It should be disposed.
Interlocked.Exchange(ref this.defaultFontResourceLock, null)?.Dispose();
// Font handles become invalid after disposing the atlas, but just to be safe.
this.DefaultFontHandle?.Dispose();
this.DefaultFontHandle = null;
this.MonoFontHandle?.Dispose();
this.MonoFontHandle = null;
this.IconFontHandle?.Dispose();
this.IconFontHandle = null;
Interlocked.Exchange(ref this.dalamudAtlas, null)?.Dispose();
Interlocked.Exchange(ref this.scene, null)?.Dispose();
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas
this.defaultFontResourceLock = null;
this.dalamudAtlas?.Dispose();
this.scene?.Dispose();
return;
void Disposer()
void ClearHooks()
{
this.setCursorHook.Dispose();
this.presentHook?.Dispose();
this.resizeBuffersHook?.Dispose();
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
Interlocked.Exchange(ref this.presentHook, null)?.Dispose();
Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose();
}
}
@ -693,7 +715,6 @@ internal class InterfaceManager : IDisposable, IServiceType
"InterfaceManager accepts event registration and stuff even when the game window is not ready.")]
private void ContinueConstruction(
TargetSigScanner sigScanner,
Framework framework,
FontAtlasFactory fontAtlasFactory)
{
this.dalamudAtlas = fontAtlasFactory
@ -731,7 +752,7 @@ internal class InterfaceManager : IDisposable, IServiceType
this.DefaultFontHandle.ImFontChanged += (_, font) =>
{
var fontLocked = font.NewRef();
Service<Framework>.Get().RunOnFrameworkThread(
this.framework.RunOnFrameworkThread(
() =>
{
// Update the ImGui default font.
@ -765,6 +786,7 @@ internal class InterfaceManager : IDisposable, IServiceType
Log.Error(ex, "Could not enable immersive mode");
}
this.setCursorHook = Hook<SetCursorDelegate>.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
this.presentHook = Hook<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
@ -808,7 +830,7 @@ internal class InterfaceManager : IDisposable, IServiceType
if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
return IntPtr.Zero;
return this.setCursorHook.IsDisposed
return this.setCursorHook?.IsDisposed is not false
? User32.SetCursor(new(hCursor, false)).DangerousGetHandle()
: this.setCursorHook.Original(hCursor);
}

View file

@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal;
[ResolveVia<ITextureProvider>]
[ResolveVia<ITextureSubstitutionProvider>]
#pragma warning restore SA1015
internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider
internal class TextureManager : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
@ -268,7 +268,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.fallbackTextureWrap?.Dispose();
this.framework.Update -= this.FrameworkOnUpdate;

View file

@ -74,7 +74,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget
this.standardEnabled = false;
if (!this.rawEnabled)
{
this.scoped.Dispose();
((IInternalDisposableService)this.scoped).DisposeService();
this.scoped = null;
}
}
@ -105,7 +105,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget
this.rawEnabled = false;
if (!this.standardEnabled)
{
this.scoped.Dispose();
((IInternalDisposableService)this.scoped).DisposeService();
this.scoped = null;
}
}
@ -135,7 +135,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget
{
if (ImGui.Button("Disable##all-disable"))
{
this.scoped?.Dispose();
((IInternalDisposableService)this.scoped)?.DisposeService();
this.scoped = null;
this.standardEnabled = this.rawEnabled = false;
}

View file

@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows;
/// A cache for plugin icons and images.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class PluginImageCache : IDisposable, IServiceType
internal class PluginImageCache : IInternalDisposableService
{
/// <summary>
/// Maximum plugin image width.
@ -136,7 +136,7 @@ internal class PluginImageCache : IDisposable, IServiceType
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.cancelToken.Cancel();
this.downloadQueue.CompleteAdding();

View file

@ -204,12 +204,12 @@ internal sealed partial class FontAtlasFactory
{
while (this.IsBuildInProgress)
await Task.Delay(100);
this.Garbage.Dispose();
this.Clear();
});
}
else
{
this.Garbage.Dispose();
this.Clear();
}
return newRefCount;
@ -227,6 +227,20 @@ internal sealed partial class FontAtlasFactory
var axisSubstance = this.Substances.OfType<GamePrebakedFontHandle.HandleSubstance>().Single();
return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild };
}
public void Clear()
{
try
{
this.Garbage.Dispose();
}
catch (Exception e)
{
Log.Error(
e,
$"Disposing {nameof(FontAtlasBuiltData)} of {this.Owner?.Name ?? "???"}.");
}
}
}
private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback
@ -547,13 +561,13 @@ internal sealed partial class FontAtlasFactory
{
if (this.buildIndex != rebuildIndex)
{
data.ExplicitDisposeIgnoreExceptions();
data.Release();
return;
}
var prevBuiltData = this.builtData;
this.builtData = data;
prevBuiltData.ExplicitDisposeIgnoreExceptions();
prevBuiltData?.Release();
this.buildTask = EmptyTask;
fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count));

View file

@ -31,7 +31,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal sealed partial class FontAtlasFactory
: IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable
: IInternalDisposableService, GamePrebakedFontHandle.IGameFontTextureProvider
{
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly CancellationTokenSource cancellationTokenSource = new();
@ -161,7 +161,7 @@ internal sealed partial class FontAtlasFactory
this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.cancellationTokenSource.Cancel();
this.scopedFinalizer.Dispose();

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
@ -291,11 +292,15 @@ internal abstract class FontHandle : IFontHandle
{
if (disposing)
{
if (Interlocked.Exchange(ref this.manager, null) is not { } managerToDisassociate)
return;
if (this.pushedFonts.Count > 0)
Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack.");
this.Manager.FreeFontHandle(this);
this.manager = null;
managerToDisassociate.FreeFontHandle(this);
this.Disposed?.InvokeSafely();
this.Disposed = null;
this.ImFontChanged = null;
}
}

View file

@ -193,7 +193,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu
#pragma warning disable SA1015
[ResolveVia<ITitleScreenMenu>]
#pragma warning restore SA1015
internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu
internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleScreenMenu
{
[ServiceManager.ServiceDependency]
private readonly TitleScreenMenu titleScreenMenuService = Service<TitleScreenMenu>.Get();
@ -204,7 +204,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc
public IReadOnlyList<TitleScreenMenuEntry>? Entries => this.titleScreenMenuService.Entries;
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
foreach (var entry in this.pluginEntries)
{

View file

@ -17,6 +17,7 @@ using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -605,6 +606,10 @@ public sealed class UiBuilder : IDisposable
}
}
/// <summary>Clean up resources allocated by this instance of <see cref="UiBuilder"/>.</summary>
/// <remarks>Dalamud internal use only.</remarks>
internal void DisposeInternal() => this.scopedFinalizer.Dispose();
/// <summary>
/// Open the registered configuration UI, if it exists.
/// </summary>

View file

@ -96,6 +96,17 @@ internal class ServiceScopeImpl : IServiceScope
/// <inheritdoc />
public void Dispose()
{
foreach (var createdObject in this.scopeCreatedObjects.OfType<IDisposable>()) createdObject.Dispose();
foreach (var createdObject in this.scopeCreatedObjects)
{
switch (createdObject)
{
case IInternalDisposableService d:
d.DisposeService();
break;
case IDisposable d:
d.Dispose();
break;
}
}
}
}

View file

@ -13,7 +13,7 @@ namespace Dalamud.Logging.Internal;
/// Class responsible for tracking asynchronous tasks.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class TaskTracker : IDisposable, IServiceType
internal class TaskTracker : IInternalDisposableService
{
private static readonly ModuleLog Log = new("TT");
private static readonly List<TaskInfo> TrackedTasksInternal = new();
@ -119,7 +119,7 @@ internal class TaskTracker : IDisposable, IServiceType
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.scheduleAndStartHook?.Dispose();

View file

@ -17,7 +17,7 @@ namespace Dalamud.Logging;
#pragma warning disable SA1015
[ResolveVia<IPluginLog>]
#pragma warning restore SA1015
internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable
internal class ScopedPluginLogService : IServiceType, IPluginLog
{
private readonly LocalPlugin localPlugin;
@ -53,12 +53,6 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable
/// </summary>
public ILogger Logger { get; }
/// <inheritdoc />
public void Dispose()
{
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Fatal(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Fatal, null, messageTemplate, values);

View file

@ -12,7 +12,7 @@ namespace Dalamud.Networking.Http;
/// awareness.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal class HappyHttpClient : IDisposable, IServiceType
internal class HappyHttpClient : IInternalDisposableService
{
/// <summary>
/// Initializes a new instance of the <see cref="HappyHttpClient"/> class.
@ -58,7 +58,7 @@ internal class HappyHttpClient : IDisposable, IServiceType
public HappyEyeballsCallback SharedHappyEyeballsCallback { get; }
/// <inheritdoc/>
void IDisposable.Dispose()
void IInternalDisposableService.DisposeService()
{
this.SharedHttpClient.Dispose();
this.SharedHappyEyeballsCallback.Dispose();

View file

@ -452,26 +452,28 @@ public sealed class DalamudPluginInterface : IDisposable
#endregion
/// <summary>
/// Unregister your plugin and dispose all references.
/// </summary>
/// <inheritdoc cref="Dispose"/>
void IDisposable.Dispose()
{
this.UiBuilder.ExplicitDispose();
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
Service<Localization>.Get().LocalizationChanged -= this.OnLocalizationChanged;
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
}
/// <summary>
/// Obsolete implicit dispose implementation. Should not be used.
/// </summary>
[Obsolete("Do not dispose \"DalamudPluginInterface\".", true)]
/// <summary>This function will do nothing. Dalamud will dispose this object on plugin unload.</summary>
[Obsolete("This function will do nothing. Dalamud will dispose this object on plugin unload.", true)]
public void Dispose()
{
// ignored
}
/// <summary>Unregister the plugin and dispose all references.</summary>
/// <remarks>Dalamud internal use only.</remarks>
internal void DisposeInternal()
{
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
Service<Localization>.Get().LocalizationChanged -= this.OnLocalizationChanged;
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
this.UiBuilder.DisposeInternal();
}
/// <summary>
/// Dispatch the active plugins changed event.
/// </summary>

View file

@ -55,7 +55,7 @@ namespace Dalamud.Plugin.Internal;
[InherentDependency<DataShare>]
#pragma warning restore SA1015
internal partial class PluginManager : IDisposable, IServiceType
internal partial class PluginManager : IInternalDisposableService
{
/// <summary>
/// Default time to wait between plugin unload and plugin assembly unload.
@ -370,7 +370,7 @@ internal partial class PluginManager : IDisposable, IServiceType
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
var disposablePlugins =
this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray();
@ -410,7 +410,16 @@ internal partial class PluginManager : IDisposable, IServiceType
// 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)
plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log);
{
try
{
plugin.Dispose();
}
catch (Exception e)
{
Log.Error(e, $"Error disposing {plugin.Name}");
}
}
}
this.assemblyLocationMonoHook?.Dispose();

View file

@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Internal.Profiles;
/// Service responsible for profile-related chat commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IServiceType, IDisposable
internal class ProfileCommandHandler : IInternalDisposableService
{
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
@ -69,7 +69,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable
}
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
this.cmd.RemoveHandler("/xlenablecollection");
this.cmd.RemoveHandler("/xldisablecollection");

View file

@ -240,7 +240,7 @@ internal class LocalPlugin : IDisposable
this.instance = null;
}
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface?.DisposeInternal();
this.DalamudInterface = null;
this.ServiceScope?.Dispose();
@ -426,7 +426,7 @@ internal class LocalPlugin : IDisposable
if (this.instance == null)
{
this.State = PluginState.LoadError;
this.DalamudInterface.ExplicitDispose();
this.DalamudInterface.DisposeInternal();
Log.Error(
$"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return;
@ -499,7 +499,7 @@ internal class LocalPlugin : IDisposable
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface?.DisposeInternal();
this.DalamudInterface = null;
this.ServiceScope?.Dispose();

View file

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -175,7 +176,8 @@ internal static class ServiceManager
foreach (var serviceType in GetConcreteServiceTypes())
{
var serviceKind = serviceType.GetServiceKind();
Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind");
CheckServiceTypeContracts(serviceType);
// Let IoC know about the interfaces this service implements
serviceContainer.RegisterInterfaces(serviceType);
@ -514,6 +516,44 @@ internal static class ServiceManager
return ServiceKind.ProvidedService;
}
/// <summary>Validate service type contracts, and throws exceptions accordingly.</summary>
/// <param name="serviceType">An instance of <see cref="Type"/> that is supposed to be a service type.</param>
/// <remarks>Does nothing on non-debug builds.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckServiceTypeContracts(Type serviceType)
{
#if DEBUG
try
{
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
throw new InvalidOperationException($"Non-{nameof(IServiceType)} passed.");
if (serviceType.GetServiceKind() == ServiceKind.None)
throw new InvalidOperationException("Service type is not specified.");
var isServiceDisposable =
serviceType.IsAssignableTo(typeof(IInternalDisposableService));
var isAnyDisposable =
isServiceDisposable
|| serviceType.IsAssignableTo(typeof(IDisposable))
|| serviceType.IsAssignableTo(typeof(IAsyncDisposable));
if (isAnyDisposable && !isServiceDisposable)
{
throw new InvalidOperationException(
$"A service must be an {nameof(IInternalDisposableService)} without specifying " +
$"{nameof(IDisposable)} nor {nameof(IAsyncDisposable)} if it is purely meant to be a service, " +
$"or an {nameof(IPublicDisposableService)} if it also is allowed to be constructed not as a " +
$"service to be used elsewhere and has to offer {nameof(IDisposable)} or " +
$"{nameof(IAsyncDisposable)}. See {nameof(ReliableFileStorage)} for an example of " +
$"{nameof(IPublicDisposableService)}.");
}
}
catch (Exception e)
{
throw new InvalidOperationException($"{serviceType.Name}: {e.Message}");
}
#endif
}
/// <summary>
/// Indicates that this constructor will be called for early initialization.
/// </summary>

View file

@ -65,6 +65,12 @@ internal static class Service<T> where T : IServiceType
None,
}
/// <summary>Does nothing.</summary>
/// <remarks>Used to invoke the static ctor.</remarks>
public static void Nop()
{
}
/// <summary>
/// Sets the type in the service locator to the given object.
/// </summary>
@ -72,6 +78,8 @@ internal static class Service<T> where T : IServiceType
public static void Provide(T obj)
{
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
if (obj is IPublicDisposableService pds)
pds.MarkDisposeOnlyFromService();
instanceTcs.SetResult(obj);
}
@ -297,23 +305,26 @@ internal static class Service<T> where T : IServiceType
if (!instanceTcs.Task.IsCompletedSuccessfully)
return;
var instance = instanceTcs.Task.Result;
if (instance is IDisposable disposable)
switch (instanceTcs.Task.Result)
{
ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name);
try
{
disposable.Dispose();
ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name);
}
catch (Exception e)
{
ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name);
}
}
else
{
ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name);
case IInternalDisposableService d:
ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name);
try
{
d.DisposeService();
ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name);
}
catch (Exception e)
{
ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name);
}
break;
default:
ServiceManager.CheckServiceTypeContracts(typeof(T));
ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name);
break;
}
instanceTcs = new TaskCompletionSource<T>();

View file

@ -27,7 +27,7 @@ namespace Dalamud.Storage.Assets;
#pragma warning disable SA1015
[ResolveVia<IDalamudAssetManager>]
#pragma warning restore SA1015
internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager
internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamudAssetManager
{
private const int DownloadAttemptCount = 10;
private const int RenameAttemptCount = 10;
@ -67,7 +67,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask()))
.ContinueWith(_ => loadTimings.Dispose()),
.ContinueWith(
r =>
{
loadTimings.Dispose();
return r;
})
.Unwrap(),
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
Task.WhenAll(
@ -83,7 +89,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
/// <inheritdoc/>
public void Dispose()
void IInternalDisposableService.DisposeService()
{
lock (this.syncRoot)
{

View file

@ -22,17 +22,22 @@ namespace Dalamud.Storage;
/// This is not an early-loaded service, as it is needed before they are initialized.
/// </remarks>
[ServiceManager.ProvidedService]
public class ReliableFileStorage : IServiceType, IDisposable
[Api10ToDo("Make internal and IInternalDisposableService, and remove #pragma guard from the caller.")]
public class ReliableFileStorage : IPublicDisposableService
{
private static readonly ModuleLog Log = new("VFS");
private readonly object syncRoot = new();
private SQLiteConnection? db;
private bool isService;
/// <summary>
/// Initializes a new instance of the <see cref="ReliableFileStorage"/> class.
/// </summary>
/// <param name="vfsDbPath">Path to the VFS.</param>
[Obsolete("Dalamud internal use only.", false)]
[Api10ToDo("Make internal, and remove #pragma guard from the caller.")]
public ReliableFileStorage(string vfsDbPath)
{
var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db");
@ -288,9 +293,20 @@ public class ReliableFileStorage : IServiceType, IDisposable
/// <inheritdoc/>
public void Dispose()
{
this.db?.Dispose();
if (!this.isService)
this.DisposeCore();
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
if (this.isService)
this.DisposeCore();
}
/// <inheritdoc/>
void IPublicDisposableService.MarkDisposeOnlyFromService() => this.isService = true;
/// <summary>
/// Replace possible non-portable parts of a path with portable versions.
/// </summary>
@ -312,6 +328,8 @@ public class ReliableFileStorage : IServiceType, IDisposable
this.db.CreateTable<DbFile>();
}
private void DisposeCore() => this.db?.Dispose();
private class DbFile
{
[PrimaryKey]

View file

@ -70,7 +70,16 @@ public static class DisposeSafety
r =>
{
if (!r.IsCompletedSuccessfully)
return ignoreAllExceptions ? Task.CompletedTask : r;
{
if (ignoreAllExceptions)
{
_ = r.Exception;
return Task.CompletedTask;
}
return r;
}
try
{
r.Result.Dispose();

View file

@ -19,7 +19,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Logging.Internal;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
@ -639,42 +638,6 @@ public static class Util
throw new Win32Exception();
}
/// <summary>
/// Dispose this object.
/// </summary>
/// <param name="obj">The object to dispose.</param>
/// <typeparam name="T">The type of object to dispose.</typeparam>
internal static void ExplicitDispose<T>(this T obj) where T : IDisposable
{
obj.Dispose();
}
/// <summary>
/// Dispose this object.
/// </summary>
/// <param name="obj">The object to dispose.</param>
/// <param name="logMessage">Log message to print, if specified and an error occurs.</param>
/// <param name="moduleLog">Module logger, if any.</param>
/// <typeparam name="T">The type of object to dispose.</typeparam>
internal static void ExplicitDisposeIgnoreExceptions<T>(
this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable
{
try
{
obj.Dispose();
}
catch (Exception e)
{
if (logMessage == null)
return;
if (moduleLog != null)
moduleLog.Error(e, logMessage);
else
Log.Error(e, logMessage);
}
}
/// <summary>
/// Gets a random, inoffensive, human-friendly string.
/// </summary>