diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 016077188..89f7aae37 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Threading; @@ -70,6 +71,8 @@ namespace Dalamud Log.Information(new string('-', 80)); Log.Information("Initializing a session.."); + var vehManager = new VehManager(0, OnVectoredException); + // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; @@ -81,6 +84,7 @@ namespace Dalamud dalamud.WaitForUnload(); dalamud.Dispose(); + vehManager.Dispose(); } catch (Exception ex) { @@ -192,6 +196,88 @@ namespace Dalamud } } + private static unsafe int OnVectoredException(ref VehManager.ExceptionPointers ex) + { + if (!Enum.IsDefined(typeof(VehManager.ExceptionCode), ex.ExceptionRecord->ExceptionCode)) + return VehManager.ExceptionContinueSearch; + + var code = (VehManager.ExceptionCode)ex.ExceptionRecord->ExceptionCode; + var address = ex.ExceptionRecord->ExceptionAddress; + var info = $"{code}(0x{ex.ExceptionRecord->ExceptionCode:X})"; + + var module = Process.GetCurrentProcess().Modules.Cast().FirstOrDefault(m => address >= m.BaseAddress && address < m.BaseAddress + m.ModuleMemorySize); + if (module != null) + { + var rva = address - module.BaseAddress; + + info += $"\nat {module.ModuleName}+{rva:X}"; + } + else + { + info += $"\nat {ex.ExceptionRecord->ExceptionAddress:X}"; + } + + const MessageBoxType flags = NativeFunctions.MessageBoxType.AbortRetryIgnore | NativeFunctions.MessageBoxType.IconError | NativeFunctions.MessageBoxType.SystemModal; + var result = MessageBoxW( + Process.GetCurrentProcess().MainWindowHandle, + $"An error within the game occurred and Dalamud handled it. This may indicate a malfunctioning plugin.\nThe game must close.\n\n{info}\n\nMore information has been recorded separately, please contact us in our Discord or on GitHub.\n\n" + + "Click \"Abort\" to save further information.\n" + + "Click \"Retry\" to disable all plugins.\n" + + "Click \"Ignore\" to do nothing and quit.", + "Dalamud", + flags); + + switch (result) + { + case (int)User32.MessageBoxResult.IDRETRY: + { + Log.Information("User chose to disable plugins on next launch..."); + var config = Service.Get(); + config.PluginSafeMode = true; + config.Save(); + break; + } + + case (int)User32.MessageBoxResult.IDABORT: + { + // TODO: We can also do this for managed exceptions, but do we want to? It requires doing dumps with full memory. + Log.Information("User chose to save minidump..."); + + #if DEBUG + var path = Path.Combine(Path.GetDirectoryName(typeof(EntryPoint).Assembly.Location), $"dalamud_appcrashd_{DateTimeOffset.Now.ToUnixTimeSeconds()}.dmp"); + #else + var path = Path.Combine(Path.GetDirectoryName(typeof(EntryPoint).Assembly.Location), "..", "..", "..", $"dalamud_appcrash_{DateTimeOffset.Now.ToUnixTimeSeconds()}.dmp"); + #endif + + try + { + var file = new FileStream(path, FileMode.Create); + + fixed (void* pEx = &ex) + { + var mdmpInfo = default(MinidumpExceptionInformation); + mdmpInfo.ClientPointers = 1; + + mdmpInfo.ExceptionPointers = new IntPtr(pEx); + mdmpInfo.ThreadId = GetCurrentThreadId(); + + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file.SafeFileHandle.DangerousGetHandle(), (int)MiniDumpType.MiniDumpWithDataSegs, ref mdmpInfo, IntPtr.Zero, IntPtr.Zero); + } + } + catch (Exception e) + { + Log.Error(e, "Failed to save minidump"); + } + + break; + } + } + + Environment.Exit(-1); + + return VehManager.ExceptionContinueSearch; + } + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) { switch (args.ExceptionObject) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index f6137dc1a..f781301fe 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -21,6 +20,8 @@ using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using PInvoke; using Serilog.Events; @@ -467,12 +468,24 @@ namespace Dalamud.Interface.Internal Process.GetCurrentProcess().Kill(); } - if (ImGui.MenuItem("Cause AccessViolation")) + ImGui.Separator(); + + if (ImGui.MenuItem("Access Violation")) { Marshal.ReadByte(IntPtr.Zero); } + if (ImGui.MenuItem("Crash game")) + { + unsafe + { + var framework = Framework.Instance(); + framework->UIModule = (UIModule*)0; + } + } + ImGui.Separator(); + if (ImGui.MenuItem("Enable Dalamud testing", string.Empty, configuration.DoDalamudTest)) { configuration.DoDalamudTest ^= true; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index ae9335e9e..33806fd73 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -369,8 +369,7 @@ namespace Dalamud.Interface.Internal if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, - $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index 38d9173f4..a4c00ba7c 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -1777,6 +1777,90 @@ namespace Dalamud byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesWritten); + + /// + /// Get a handle to the current process. + /// + /// Handle to the process. + [DllImport("kernel32.dll")] + public static extern IntPtr GetCurrentProcess(); + + /// + /// Get the current process ID. + /// + /// The process ID. + [DllImport("kernel32.dll")] + public static extern uint GetCurrentProcessId(); + + /// + /// Get the current thread ID. + /// + /// The thread ID. + [DllImport("kernel32.dll")] + public static extern uint GetCurrentThreadId(); + } + + /// + /// Native dbghelp functions. + /// + internal static partial class NativeFunctions + { + /// + /// Type of minidump to create. + /// + public enum MiniDumpType : int + { + /// + /// Normal minidump. + /// + MiniDumpNormal, + + /// + /// Minidump with data segments. + /// + MiniDumpWithDataSegs, + + /// + /// Minidump with full memory. + /// + MiniDumpWithFullMemory, + } + + /// + /// Creates a minidump. + /// + /// Target process handle. + /// Target process ID. + /// Output file handle. + /// Type of dump to take. + /// Exception information. + /// User information. + /// Callback. + /// Whether or not the minidump succeeded. + [DllImport("dbghelp.dll")] + public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, IntPtr hFile, int dumpType, ref MinidumpExceptionInformation exceptionInfo, IntPtr userStreamParam, IntPtr callback); + + /// + /// Structure describing minidump exception information. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct MinidumpExceptionInformation + { + /// + /// ID of the thread that caused the exception. + /// + public uint ThreadId; + + /// + /// Pointer to the exception record. + /// + public IntPtr ExceptionPointers; + + /// + /// ClientPointers field. + /// + public int ClientPointers; + } } /// diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2c58a7710..a16eeb40b 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -5,11 +5,11 @@ using System.IO.Compression; using System.Linq; using System.Reflection; using System.Text; + using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Logging.Internal; using ImGuiNET; using Microsoft.Win32; using Serilog; diff --git a/Dalamud/VehManager.cs b/Dalamud/VehManager.cs new file mode 100644 index 000000000..b3adc99c1 --- /dev/null +++ b/Dalamud/VehManager.cs @@ -0,0 +1,303 @@ +using System.Runtime.InteropServices; + +namespace Dalamud +{ + /// + /// Class encapsulating a vectored exception handler. + /// + internal unsafe class VehManager + { + /// + /// Execute the exception handler. + /// + public const int ExceptionExecuteHandler = 1; + + /// + /// Continue the search for another handler. + /// + public const int ExceptionContinueSearch = 0; + + /// + /// Continue execution after the handler. + /// + public const int ExceptionContinueExecution = -1; + + private VectoredExceptionHandler? myHandler; // to keep a reference, just in case, idk if it's needed + + /// + /// Initializes a new instance of the class. + /// + /// Position of this VEH. 0 = Last, 1 = First. + /// The handler to register. + public VehManager(uint first, VectoredExceptionHandler myHandler) + { + this.myHandler = myHandler; + this.Handle = AddVectoredExceptionHandler(first, this.myHandler); + } + + /// + /// VEH Delegate. + /// + /// Exception information. + /// Code that determines which action to take. + public delegate int VectoredExceptionHandler(ref ExceptionPointers ex); + + /// + /// Gets the handle to the VEH. + /// + public nint Handle { get; private set; } + + /// + /// Dispose and remove this VEH. + /// + public void Dispose() + { + if (this.Handle == 0) + return; + + if (!RemoveVectoredExceptionHandler(this.Handle)) + return; + + this.Handle = 0; + this.myHandler = null; + } + + #region DllImports + + [DllImport("kernel32", SetLastError = true)] + private static extern nint AddVectoredExceptionHandler(uint first, VectoredExceptionHandler handler); + + [DllImport("kernel32", SetLastError = true)] + private static extern bool RemoveVectoredExceptionHandler(nint handle); + + #endregion + +#pragma warning disable SA1600 +#pragma warning disable SA1602 +#pragma warning disable SA1201 + + #region Enums + + public enum ExceptionCode : uint + { + AccessViolation = 0xC0000005, + InPageError = 0xC0000006, + InvalidHandle = 0xC0000008, + InvalidParameter = 0xC000000D, + NoMemory = 0xC0000017, + IllegalInstruction = 0xC000001D, + NoncontinuableException = 0xC0000025, + InvalidDisposition = 0xC0000026, + ArrayBoundsExceeded = 0xC000008C, + FloatDenormalOperand = 0xC000008D, + FloatDivideByZero = 0xC000008E, + FloatInexactResult = 0xC000008F, + FloatInvalidOperation = 0xC0000090, + FloatOverflow = 0xC0000091, + FloatStackCheck = 0xC0000092, + FloatUnderflow = 0xC0000093, + IntegerDivideByZero = 0xC0000094, + IntegerOverflow = 0xC0000095, + PrivilegedInstruction = 0xC0000096, + StackOverflow = 0xC00000FD, + DllNotFound = 0xC0000135, + OrdinalNotFound = 0xC0000138, + EntrypointNotFound = 0xC0000139, + ControlCExit = 0xC000013A, + DllInitFailed = 0xC0000142, + ControlStackViolation = 0xC00001B2, + FloatMultipleFaults = 0xC00002B4, + FloatMultipleTraps = 0xC00002B5, + RegNatConsumption = 0xC00002C9, + HeapCorruption = 0xC0000374, + StackBufferOverrun = 0xC0000409, + InvalidCruntimeParameter = 0xC0000417, + AssertionFailure = 0xC0000420, + EnclaveViolation = 0xC00004A2, + Interrupted = 0xC0000515, + ThreadNotRunning = 0xC0000516, + AlreadyRegistered = 0xC0000718, + } + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionPointers + { + public ExceptionRecord64* ExceptionRecord; + public Context* ContextRecord; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionRecord64 + { + public uint ExceptionCode; + public uint ExceptionFlags; + public nint ExceptionRecord; + public nint ExceptionAddress; + public uint NumberParameters; + public uint UnusedAlignment; + public fixed ulong ExceptionInformation[15]; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Context + { + // Register parameter home addresses. + // + // N.B. These fields are for convience - they could be used to extend the + // context record in the future. + + public nint P1Home; + public nint P2Home; + public nint P3Home; + public nint P4Home; + public nint P5Home; + public nint P6Home; + + // Control flags. + + public uint ContextFlags; + public uint MxCsr; + + // Segment Registers and processor flags. + + public ushort SegCs; + public ushort SegDs; + public ushort SegEs; + public ushort SegFs; + public ushort SegGs; + public ushort SegSs; + public uint EFlags; + + // Debug registers + + public nint Dr0; + public nint Dr1; + public nint Dr2; + public nint Dr3; + public nint Dr6; + public nint Dr7; + + // Integer registers. + + public nint Rax; + public nint Rcx; + public nint Rdx; + public nint Rbx; + public nint Rsp; + public nint Rbp; + public nint Rsi; + public nint Rdi; + public nint R8; + public nint R9; + public nint R10; + public nint R11; + public nint R12; + public nint R13; + public nint R14; + public nint R15; + + // Program counter. + + public nint Rip; + + // Floating point state. + + public XmmRegisters FloatRegisters; + + // Vector registers. + + public VectorRegisters VectorRegister; + public nint VectorControl; + + // Special debug control registers. + + public nint DebugControl; + public nint LastBranchToRip; + public nint LastBranchFromRip; + public nint LastExceptionToRip; + + public nint LastExceptionFromRip; + } + + [StructLayout(LayoutKind.Sequential)] + public struct M128A + { + public ulong Low; + public long High; + } + + [StructLayout(LayoutKind.Sequential)] + public struct VectorRegisters + { + public M128A V0; + public M128A V1; + public M128A V2; + public M128A V3; + public M128A V4; + public M128A V5; + public M128A V6; + public M128A V7; + public M128A V8; + public M128A V9; + public M128A V10; + public M128A V11; + public M128A V12; + public M128A V13; + public M128A V14; + public M128A V15; + public M128A V16; + public M128A V17; + public M128A V18; + public M128A V19; + public M128A V20; + public M128A V21; + public M128A V22; + public M128A V23; + public M128A V24; + public M128A V25; + } + + [StructLayout(LayoutKind.Sequential)] + public struct XmmRegisters + { + public readonly M128A Header0; + public readonly M128A Header1; + + public M128A Float0; + public M128A Float1; + public M128A Float2; + public M128A Float3; + public M128A Float4; + public M128A Float5; + public M128A Float6; + public M128A Float7; + + public M128A Xmm0; + public M128A Xmm1; + public M128A Xmm2; + public M128A Xmm3; + public M128A Xmm4; + public M128A Xmm5; + public M128A Xmm6; + public M128A Xmm7; + public M128A Xmm8; + public M128A Xmm9; + public M128A Xmm10; + public M128A Xmm11; + public M128A Xmm12; + public M128A Xmm13; + public M128A Xmm14; + public M128A Xmm15; + } + + #endregion + +#pragma warning restore SA1600 +#pragma warning restore SA1602 +#pragma warning restore SA1201 + } +}