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

@ -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);
}