From 672793b6c0697f9bba4b79da920fbcab597525b9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 17 Apr 2025 17:14:25 +0200 Subject: [PATCH] Add hook stress test --- Dalamud/Hooking/Internal/CallHook.cs | 24 ++- .../Windows/Data/Widgets/HookWidget.cs | 181 +++++++++++++++++- 2 files changed, 189 insertions(+), 16 deletions(-) diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index c9b5562ba..5b438b5a8 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -15,10 +15,10 @@ namespace Dalamud.Hooking.Internal; /// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. /// /// Delegate signature for this hook. -internal class CallHook : IDisposable where T : Delegate +internal class CallHook : IDalamudHook where T : Delegate { private readonly Reloaded.Hooks.AsmHook asmHook; - + private T? detour; private bool activated; @@ -29,7 +29,10 @@ internal class CallHook : IDisposable where T : Delegate /// Delegate to invoke. internal CallHook(nint address, T detour) { + ArgumentNullException.ThrowIfNull(detour); + this.detour = detour; + this.Address = address; var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); var code = new[] @@ -38,14 +41,14 @@ internal class CallHook : IDisposable where T : Delegate $"mov rax, 0x{detourPtr:X8}", "call rax", }; - + var opt = new AsmHookOptions { PreferRelativeJump = true, Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, MaxOpcodeSize = 5, }; - + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); } @@ -53,7 +56,16 @@ internal class CallHook : IDisposable where T : Delegate /// Gets a value indicating whether or not the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; - + + /// + public IntPtr Address { get; } + + /// + public string BackendName => "Reloaded AsmHook"; + + /// + public bool IsDisposed => this.detour == null; + /// /// Starts intercepting a call to the function. /// @@ -65,7 +77,7 @@ internal class CallHook : IDisposable where T : Delegate this.asmHook.Activate(); return; } - + this.asmHook.Enable(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index b24587d6c..ec5f12d6e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,6 +1,15 @@ -using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.Addon.Lifecycle; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; + +using FFXIVClientStructs.FFXIV.Component.GUI; + using ImGuiNET; using PInvoke; using Serilog; @@ -10,23 +19,46 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal class HookWidget : IDataWindowWidget +internal unsafe class HookWidget : IDataWindowWidget { + private readonly List hookStressTestList = []; + private Hook? messageBoxMinHook; private bool hookUseMinHook; - + + private int hookStressTestCount = 0; + private int hookStressTestMax = 1000; + private int hookStressTestWait = 100; + private int hookStressTestMaxDegreeOfParallelism = 10; + private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random; + private bool hookStressTestRunning = false; + + private MessageBoxWDelegate? messageBoxWOriginal; + private AddonFinalizeDelegate? addonFinalizeOriginal; + + private AddonLifecycleAddressResolver? address; + private delegate int MessageBoxWDelegate( IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string text, [MarshalAs(UnmanagedType.LPWStr)] string caption, NativeFunctions.MessageBoxType type); - + + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + private enum StressTestHookTarget + { + MessageBoxW, + AddonFinalize, + Random, + } + /// - public string DisplayName { get; init; } = "Hook"; + public string DisplayName { get; init; } = "Hook"; /// public string[]? CommandShortcuts { get; init; } = { "hook" }; - + /// public bool Ready { get; set; } @@ -34,6 +66,9 @@ internal class HookWidget : IDataWindowWidget public void Load() { this.Ready = true; + + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(Service.Get()); } /// @@ -41,7 +76,9 @@ internal class HookWidget : IDataWindowWidget { try { - ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); + ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook); + + ImGui.Separator(); if (ImGui.Button("Create")) this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); @@ -66,18 +103,94 @@ internal class HookWidget : IDataWindowWidget if (this.messageBoxMinHook != null) ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); + + ImGui.Separator(); + + ImGui.BeginDisabled(this.hookStressTestRunning); + ImGui.Text("Stress Test"); + + if (ImGui.InputInt("Max", ref this.hookStressTestMax)) + this.hookStressTestCount = 0; + + ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait); + ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism); + + if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget))) + { + foreach (var target in Enum.GetValues()) + { + if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target)) + this.hookStressTestHookTarget = target; + } + + ImGui.EndCombo(); + } + + if (ImGui.Button("Stress Test")) + { + Task.Run(() => + { + this.hookStressTestRunning = true; + this.hookStressTestCount = 0; + Parallel.For( + 0, + this.hookStressTestMax, + new ParallelOptions + { + MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism, + }, + _ => + { + this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget)); + this.hookStressTestCount++; + Thread.Sleep(this.hookStressTestWait); + }); + }).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Stress test failed"); + } + else + { + Log.Information("Stress test completed"); + } + + this.hookStressTestRunning = false; + this.hookStressTestList.ForEach(hook => + { + hook.Dispose(); + }); + this.hookStressTestList.Clear(); + }); + } + + ImGui.EndDisabled(); + + ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle")); + ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}"); } catch (Exception ex) { - Log.Error(ex, "MinHook error caught"); + Log.Error(ex, "Hook error caught"); } } - + + private static string HookTargetToString(StressTestHookTarget target) + { + return target switch + { + StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", + StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", + _ => target.ToString(), + }; + } + private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, NativeFunctions.MessageBoxType type) { Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type); - var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); + var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); if (result == (int)User32.MessageBoxResult.IDYES) { @@ -86,4 +199,52 @@ internal class HookWidget : IDataWindowWidget return result; } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + Log.Information("OnAddonFinalize"); + this.addonFinalizeOriginal!(unitManager, atkUnitBase); + } + + private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) + { + Log.Information("OnAddonUpdate"); + } + + private IDalamudHook HookMessageBoxW() + { + var hook = Hook.FromSymbol( + "User32", + "MessageBoxW", + this.MessageBoxWDetour, + this.hookUseMinHook); + + this.messageBoxWOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookAddonFinalize() + { + var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + + this.addonFinalizeOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookTarget(StressTestHookTarget target) + { + if (target == StressTestHookTarget.Random) + { + target = (StressTestHookTarget)Random.Shared.Next(0, 2); + } + + return target switch + { + StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), + StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), + _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), + }; + } }