Keep track of hooks and display them in the Plugin Stats window

This will make it easier to find plugins that are not correctly removing their hooks
This commit is contained in:
Cara 2020-12-15 02:57:04 +10:30
parent f1f7803434
commit 850123aece
4 changed files with 130 additions and 6 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using EasyHook;
@ -9,9 +10,7 @@ namespace Dalamud.Hooking {
/// This class is basically a thin wrapper around the LocalHook type to provide helper functions.
/// </summary>
/// <typeparam name="T">Delegate type to represents a function prototype. This must be the same prototype as original function do.</typeparam>
public sealed class Hook<T> : IDisposable where T : Delegate {
private bool isDisposed;
public sealed class Hook<T> : IDisposable, IDalamudHook where T : Delegate {
private readonly IntPtr address;
private readonly T original;
@ -69,19 +68,20 @@ namespace Dalamud.Hooking {
this.hookInfo = LocalHook.Create(address, detour, callbackParam); // Installs a hook here
this.address = address;
this.original = Marshal.GetDelegateForFunctionPointer<T>(this.hookInfo.HookBypassAddress);
HookInfo.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly()});
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose() {
if (this.isDisposed) {
if (this.IsDisposed) {
return;
}
this.hookInfo.Dispose();
this.isDisposed = true;
this.IsDisposed = true;
}
/// <summary>
@ -104,9 +104,24 @@ namespace Dalamud.Hooking {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckDisposed() {
if (this.isDisposed) {
if (this.IsDisposed) {
throw new ObjectDisposedException("Hook is already disposed.");
}
}
/// <summary>
/// Check if the hook is enabled.
/// </summary>
public bool IsEnabled {
get {
CheckDisposed();
return this.hookInfo.ThreadACL.IsExclusive;
}
}
/// <summary>
/// Check if the hook has been disposed
/// </summary>
public bool IsDisposed { get; private set; }
}
}

View file

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
namespace Dalamud.Hooking {
internal class HookInfo {
internal static List<HookInfo> TrackedHooks = new List<HookInfo>();
internal IDalamudHook Hook { get; set; }
internal Delegate Delegate { get; set; }
internal Assembly Assembly { get; set; }
private ulong? inProcessMemory = 0;
internal ulong? InProcessMemory {
get {
if (Hook.IsDisposed) return 0;
if (this.inProcessMemory == null) return null;
if (this.inProcessMemory.Value > 0) return this.inProcessMemory.Value;
var p = Process.GetCurrentProcess().MainModule;
var begin = (ulong) p.BaseAddress.ToInt64();
var end = begin + (ulong) p.ModuleMemorySize;
var hookAddr = (ulong) Hook.Address.ToInt64();
if (hookAddr >= begin && hookAddr <= end) {
this.inProcessMemory = hookAddr - begin;
return this.inProcessMemory.Value;
} else {
this.inProcessMemory = null;
return null;
}
}
}
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Dalamud.Hooking {
internal interface IDalamudHook {
public IntPtr Address { get; }
public bool IsEnabled { get; }
public bool IsDisposed { get; }
}
}

View file

@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Reflection;
using Dalamud.Game.Internal;
using Dalamud.Hooking;
using Dalamud.Plugin;
using ImGuiNET;
@ -8,6 +10,8 @@ namespace Dalamud.Interface {
internal class DalamudPluginStatWindow : IDisposable {
private readonly PluginManager pluginManager;
private bool showDalamudHooks;
public DalamudPluginStatWindow(PluginManager pluginManager) {
this.pluginManager = pluginManager;
}
@ -122,10 +126,70 @@ namespace Dalamud.Interface {
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Hooks")) {
ImGui.Columns(3);
ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 280);
ImGui.SetColumnWidth(1, 180f);
ImGui.SetColumnWidth(2, 100f);
ImGui.Text("Detour Method");
ImGui.SameLine();
ImGui.Text(" ");
ImGui.SameLine();
ImGui.Checkbox("Show Dalamud Hooks ###showDalamudHooksCheckbox", ref showDalamudHooks);
ImGui.NextColumn();
ImGui.Text("Address");
ImGui.NextColumn();
ImGui.Text("Status");
ImGui.NextColumn();
ImGui.Separator();
ImGui.Separator();
foreach (var trackedHook in HookInfo.TrackedHooks) {
try {
if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue;
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
ImGui.NextColumn();
if (!trackedHook.Hook.IsDisposed) {
ImGui.Text($"{trackedHook.Hook.Address.ToInt64():X}");
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue) {
ImGui.Text($"ffxiv_dx11.exe + {processMemoryOffset:X}");
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
}
}
ImGui.NextColumn();
if (trackedHook.Hook.IsDisposed) {
ImGui.Text("Disposed");
} else {
ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled");
}
ImGui.NextColumn();
} catch (Exception ex) {
ImGui.Text(ex.Message);
ImGui.NextColumn();
while (ImGui.GetColumnIndex() != 0) ImGui.NextColumn();
}
ImGui.Separator();
}
ImGui.Columns();
}
if (ImGui.IsWindowAppearing()) {
HookInfo.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed);
}
ImGui.EndTabBar();
ImGui.End();