diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index d20265bf86..21ee5c2014 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -74,11 +74,13 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) } else if (json.is_string()) { - const auto langstr = unicode::convert(json.get(), &unicode::lower); - if (langstr == "entrypoint") + const auto loadstr = unicode::convert(json.get(), &unicode::lower); + if (loadstr == "entrypoint") value = DalamudStartInfo::LoadMethod::Entrypoint; - else if (langstr == "inject") + else if (loadstr == "inject") value = DalamudStartInfo::LoadMethod::DllInject; + else if (loadstr == "hybrid") + value = DalamudStartInfo::LoadMethod::Hybrid; } } diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 5cee8f16b7..8ae0116bdf 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -29,6 +29,7 @@ struct DalamudStartInfo { enum class LoadMethod : int { Entrypoint, DllInject, + Hybrid, }; friend void from_json(const nlohmann::json&, LoadMethod&); diff --git a/Dalamud.Boot/module.def b/Dalamud.Boot/module.def index 047d825e54..3a12618ebc 100644 --- a/Dalamud.Boot/module.def +++ b/Dalamud.Boot/module.def @@ -1,5 +1,6 @@ LIBRARY Dalamud.Boot EXPORTS Initialize @1 - RewriteRemoteEntryPointW @2 - RewrittenEntryPoint @3 + GetRemoteEntryPointW @2 + RewriteRemoteEntryPointW @3 + RewrittenEntryPoint @4 diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index ef1827a088..7b53da1055 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -229,9 +229,9 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path /// @brief Get the target process' entry point. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. -/// @return address to entry point; null if successful. +/// @return address to entry point; null if unsuccessful. /// -extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { +extern "C" char* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { try { const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); @@ -246,7 +246,7 @@ extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcw const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); - return static_cast(entrypoint); + return entrypoint; } catch (const std::exception& e) { logging::E("Failed to retrieve entry point for 0x{:X}: {}", hProcess, e.what()); @@ -295,7 +295,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ auto& params = *reinterpret_cast(buffer.data()); params.entrypointLength = entrypoint_replacement.size(); - params.pEntrypoint = static_cast(entrypoint); + params.pEntrypoint = entrypoint; // Backup original entry point. last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size()); diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index ade295d02a..f889d68103 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -111,6 +111,9 @@ static void append_injector_launch_args(std::vector& args) break; case DalamudStartInfo::LoadMethod::DllInject: args.emplace_back(L"--mode=inject"); + break; + case DalamudStartInfo::LoadMethod::Hybrid: + args.emplace_back(L"--mode=hybrid"); } args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert(g_startInfo.WorkingDirectory) + L"\""); diff --git a/Dalamud.Common/LoadMethod.cs b/Dalamud.Common/LoadMethod.cs index ca50098e28..ad1ebe03b6 100644 --- a/Dalamud.Common/LoadMethod.cs +++ b/Dalamud.Common/LoadMethod.cs @@ -14,4 +14,9 @@ public enum LoadMethod /// Load Dalamud via DLL-injection. /// DllInject, + + /// + /// Load Dalamud via DLL-injection at the suspended entrypoint. + /// + Hybrid, } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 82bfe4fb5f..a9007733dd 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -701,6 +701,10 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala { dalamudStartInfo.LoadMethod = LoadMethod.DllInject; } + else if (mode.Length > 0 && mode.Length <= 6 && "hybrid"[0..mode.Length] == mode) + { + dalamudStartInfo.LoadMethod = LoadMethod.Hybrid; + } else { throw new CommandLineException($"\"{mode}\" is not a valid Dalamud load mode."); @@ -804,6 +808,12 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); } + var entryPoint = nint.Zero; + var entryPointInstruction = new byte[1]; + + // waiting with a suspended main thread will always deadlock + waitForGameWindow = (dalamudStartInfo.LoadMethod != LoadMethod.Hybrid) && waitForGameWindow; + var (process, mainThreadHandle) = GameStart.LaunchGame( Path.GetDirectoryName(gamePath), gamePath, @@ -819,12 +829,53 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo))); Log.Verbose("RewriteRemoteEntryPointW called!"); } + + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid) + { + DebugSetProcessKillOnExit(false); + entryPoint = GetRemoteEntryPointW(p.Handle, gamePath); + if (entryPoint == nint.Zero) + throw new EntryPointNotFoundException("Couldn't get game process entry point"); + Log.Verbose("Got remote entry point {0}", entryPoint); + var int3 = new byte[] { 0xCC }; + VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection); + Log.Verbose("Original entry point protection {0}", entryPointProtection); + ReadProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesRead); + Log.Verbose("Original entry point instruction {0}", entryPointInstruction[0]); + WriteProcessMemory(p.Handle, entryPoint, int3, 1, out var numberOfBytesWritten); + if (numberOfBytesWritten != 1) + throw new Exception("Failed to set breakpoint at entry point"); + VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _); + FlushInstructionCache(p.Handle, entryPoint, 1); + } + }, + (p, mt) => + { + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid) + { + var dwThreadId = WaitForBreakpointDebugEvent(entryPoint); + VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection); + WriteProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesWritten); + if (numberOfBytesWritten != 1) + throw new Exception("Failed to restore entry point instruction"); + VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _); + FlushInstructionCache(p.Handle, entryPoint, 1); + SuspendThread(mt); + if (!ContinueDebugEvent((uint)p.Id, dwThreadId, DBG_CONTINUE)) + throw new Exception("ContinueDebugEvent failed"); + if (DbgUiStopDebugging(p.Handle) != 0) + { + throw new Exception("Couldn't detach from target"); + } + } }, - waitForGameWindow); + waitForGameWindow, + !withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid); Log.Verbose("Game process started with PID {0}", process.Id); - if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject) + if (!withoutDalamud && (dalamudStartInfo.LoadMethod == LoadMethod.DllInject || + dalamudStartInfo.LoadMethod == LoadMethod.Hybrid)) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -846,6 +897,54 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala return 0; } + private static uint WaitForBreakpointDebugEvent(nint exceptionAddress) + { + var dwThreadId = 0U; + var debugEventPtr = Marshal.AllocHGlobal(188); + + Log.Verbose("Waiting for debug breakpoint event..."); + + while (WaitForDebugEvent(debugEventPtr, uint.MaxValue)) + { + var debugEvent = Marshal.PtrToStructure(debugEventPtr); + + Log.Verbose("Got debug event code: {0}", debugEvent.dwDebugEventCode); + + if (debugEvent.dwDebugEventCode != EXCEPTION_DEBUG_EVENT) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionCode != EXCEPTION_BREAKPOINT) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + Log.Debug("Got debug breakpoint event with adress {0}", debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress); + + if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress != exceptionAddress) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + dwThreadId = debugEvent.dwThreadId; + Log.Information("Got entry point debug breakpoint event for TID {0}", dwThreadId); + break; + } + + Marshal.FreeHGlobal(debugEventPtr); + if (dwThreadId == 0) + { + Log.Error("Failed to wait for debug event"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + return dwThreadId; + } + private static Process GetInheritableCurrentProcessHandle() { if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) @@ -975,6 +1074,9 @@ private static void Inject(Process process, DalamudStartInfo startInfo, nint mai Log.Information("Done"); } + [DllImport("Dalamud.Boot.dll")] + private static extern nint GetRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath); + [DllImport("Dalamud.Boot.dll")] private static extern int RewriteRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath, [MarshalAs(UnmanagedType.LPWStr)] string loadInfoJson); diff --git a/Dalamud.Injector/GameStart.cs b/Dalamud.Injector/GameStart.cs index d5c82b30a4..6f82f6f5f0 100644 --- a/Dalamud.Injector/GameStart.cs +++ b/Dalamud.Injector/GameStart.cs @@ -24,11 +24,13 @@ public static class GameStart /// Arguments to pass to the executable file. /// Don't actually fix the ACL. /// Action to execute before the process is started. + /// Action to execute after the process is started. /// Wait for the game window to be ready before proceeding. + /// Attach to the game process as debugger. /// The started process and handle to the started main thread. /// Thrown when a win32 error occurs. /// Thrown when the process did not start correctly. - public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) + public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, Action afterResume, bool waitForGameWindow = true, bool attach = false) { Process process = null; @@ -98,7 +100,7 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor ref lpProcessAttributes, IntPtr.Zero, false, - PInvoke.CREATE_SUSPENDED, + attach ? PInvoke.DEBUG_ONLY_THIS_PROCESS : PInvoke.CREATE_SUSPENDED, IntPtr.Zero, workingDir, ref lpStartupInfo, @@ -121,6 +123,8 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor PInvoke.ResumeThread(lpProcessInformation.hThread); + afterResume?.Invoke(process, lpProcessInformation.hThread); + // Ensure that the game main window is prepared if (waitForGameWindow) { @@ -174,7 +178,15 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor Marshal.FreeHGlobal(psecDesc); } - NativeFunctions.DuplicateHandle(PInvoke.GetCurrentProcess(), lpProcessInformation.hThread, lpProcessInformation.hProcess, out var mainThreadHandle, 0, false, NativeFunctions.DuplicateOptions.SameAccess); + NativeFunctions.DuplicateHandle( + PInvoke.GetCurrentProcess(), + lpProcessInformation.hThread, + lpProcessInformation.hProcess, + out var mainThreadHandle, + 0, + false, + NativeFunctions.DuplicateOptions.SameAccess); + PInvoke.CloseHandle(lpProcessInformation.hThread); return (process, mainThreadHandle); @@ -360,6 +372,7 @@ private static class PInvoke public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1; + public const UInt32 DEBUG_ONLY_THIS_PROCESS = 0x00000002; public const UInt32 CREATE_SUSPENDED = 0x00000004; public const UInt32 TOKEN_QUERY = 0x0008; diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index 2a4654aafe..9d911e6d12 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -758,6 +758,47 @@ public static extern IntPtr VirtualAllocEx( AllocationType flAllocationType, MemoryProtection flProtect); + /// + /// Changes the protection on a region of committed pages in the virtual address space of a specified process. + /// For more information, see https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex. + /// + /// + /// The handle to the process whose memory protection is to be changed. The handle must have the PROCESS_VM_OPERATION access + /// right. + /// + /// + /// A pointer to the base address of the region of pages whose access protection attributes are to be changed. All pages in + /// the specified region must be within the same reserved region allocated when calling the VirtualAlloc or VirtualAllocEx + /// function using MEM_RESERVE. The pages cannot span reserved regions that were allocated by separate calls to VirtualAlloc + /// or VirtualAllocEx using MEM_RESERVE. + /// + /// + /// The size of the region whose access protection attributes are changed, in bytes. The region of affected pages includes + /// all pages containing one or more bytes in the range from the lpAddress parameter to (lpAddress+dwSize). This means that + /// a 2-byte range straddling a page boundary causes the protection attributes of both pages to be changed. + /// + /// + /// The memory protection option. This parameter can be one of the memory protection constants. For the most common use, to + /// execute code in the region, the memory must be marked as PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE or + /// PAGE_EXECUTE_WRITECOPY. + /// + /// + /// A pointer to a variable that receives the previous access protection value of the first page in the specified region of + /// pages. If this parameter is NULL or does not point to a valid variable, the function fails. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool VirtualProtectEx( + IntPtr hProcess, + IntPtr lpAddress, + UIntPtr dwSize, + MemoryProtection flNewProtect, + out MemoryProtection lpflOldProtect); + + /// /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex. /// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified @@ -815,6 +856,153 @@ public static extern bool VirtualFreeEx( [DllImport("kernel32.dll", SetLastError = true)] public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + public const uint EXCEPTION_DEBUG_EVENT = 1; + public const uint EXCEPTION_BREAKPOINT = 0x80000003; + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionRecord + { + public uint ExceptionCode; + public uint ExceptionFlags; + public IntPtr NextExceptionRecord; + public IntPtr ExceptionAddress; + public uint NumberParameters; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 15, ArraySubType = UnmanagedType.U4)] + public uint[] ExceptionInformation; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionDebugInfo + { + public ExceptionRecord ExceptionRecord; + public uint dwFirstChance; + } + + [StructLayout(LayoutKind.Sequential)] + public struct DebugEvent + { + public uint dwDebugEventCode; + public uint dwProcessId; + public uint dwThreadId; + public uint dw64PlatformPadding; + public ExceptionDebugInfo ExceptionDebugInfo; + } + + /// + /// Waits for a debugging event to occur in a debugged process. + /// + /// + /// Out parameter that receives information about the debugging event. + /// + /// + /// The time-out interval, in milliseconds. If this parameter is zero, the function tests for a debugging event and returns + /// immediately. If the parameter is INFINITE, the function does not return until a debugging event + /// has occurred. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WaitForDebugEvent(nint lpDebugEvent, uint dwMilliseconds); + + /// + /// If the thread specified by the dwThreadId parameter previously reported an EXCEPTION_DEBUG_EVENT debugging event, the function + /// stops all exception processing and continues the thread and the exception is marked as handled. For any other debugging event, + /// this flag simply continues the thread. + /// + public const uint DBG_CONTINUE = 0x00010002; + + /// + /// Enables a debugger to continue a thread that previously reported a debugging event. + /// + /// + /// The identifier of the process in which the debug event occurred. + /// + /// + /// The identifier of the thread that generated the debug event. + /// + /// + /// The flags that control how the debugging continues. Use DBG_CONTINUE for most debug events, including EXCEPTION_DEBUG_EVENT. + /// For exceptions, you can also use DBG_EXCEPTION_NOT_HANDLED to indicate the debugger did not handle the exception. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError + /// . + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ContinueDebugEvent( + uint dwProcessId, + uint dwThreadId, + uint dwContinueStatus); + + /// + /// Sets a flag indicating whether a debugged process is to be terminated when the debugger exits. + /// + /// + /// If this parameter is TRUE, the debugged process is terminated when the debugger exits. If it is FALSE, the debugged process + /// continues to run after the debugger exits. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DebugSetProcessKillOnExit(bool killOnExit); + + /// + /// Stops the debugger from debugging the specified process. + /// + /// + /// The handle of the process from which the debugger will detach. + /// + /// + /// The function returns an NTSTATUS. + /// + [DllImport("ntdll.dll")] + public static extern uint DbgUiStopDebugging(IntPtr hProcess); + + /// + /// Suspends a thread. + /// + /// Handle to the thread to be suspended. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint SuspendThread(IntPtr hThread); + + /// + /// Reads data from an area of memory in a specified process. The entire area to be read must be accessible, or the operation fails. + /// + /// + /// A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process. + /// + /// + /// A pointer to the base address in the specified process from which to read. Before the data transfer occurs, the system verifies + /// that all data in the specified memory range is accessible for read access, and if it is not accessible, the function fails. + /// + /// + /// A pointer to a buffer that receives the data read from the address space of the specified process. + /// + /// + /// The number of bytes to be read from the specified process. + /// + /// + /// A pointer to a variable that receives the number of bytes transferred into the specified buffer. This parameter is optional. + /// If lpNumberOfBytesRead is NULL, the parameter is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get extended error + /// information, call GetLastError. The function fails if the requested read operation crosses into an area of the process that is inaccessible. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + [Out] byte[] lpBuffer, + int dwSize, + out IntPtr lpNumberOfBytesRead); + /// /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or /// the operation fails. @@ -851,6 +1039,33 @@ public static extern bool WriteProcessMemory( int dwSize, out IntPtr lpNumberOfBytesWritten); + /// + /// Flushes the instruction cache for the specified process. This function is necessary if you are generating or + /// modifying code in memory for a process that is currently running. + /// For more information, see https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-flushinstructioncache. + /// + /// + /// A handle to a process whose instruction cache is to be flushed. This handle must have the PROCESS_VM_OPERATION + /// access right. For more information, see Process Security and Access Rights. + /// + /// + /// A pointer to the base of the region to be flushed. This parameter can be NULL. + /// + /// + /// The size of the region to be flushed if the lpBaseAddress parameter is not NULL, in bytes. If lpBaseAddress is NULL, + /// dwSize is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool FlushInstructionCache( + IntPtr hProcess, + IntPtr lpBaseAddress, + int dwSize); + + /// /// Duplicates an object handle. /// diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 5254e041c4..c4e79148b0 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -168,19 +168,25 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadHandle, Ma } var currentSuspendCount = (int)NativeFunctions.SuspendThread(mainThreadHandle) + 1; + Log.Verbose("Current main thread suspend count {0}", currentSuspendCount); suspendSignal.Set(); switch (info.LoadMethod) { case LoadMethod.Entrypoint: if (currentSuspendCount != 1) - Log.Warning("Unexpected suspend count {} for main thread with Entrypoint", currentSuspendCount); + Log.Warning("Unexpected suspend count {0} for main thread with Entrypoint", currentSuspendCount); break; case LoadMethod.DllInject: if (currentSuspendCount != 1) - Log.Warning("Unexpected suspend count {} for main thread with DllInject", currentSuspendCount); + Log.Warning("Unexpected suspend count {0} for main thread with DllInject", currentSuspendCount); + break; + case LoadMethod.Hybrid: + if (currentSuspendCount != 2) + Log.Warning("Unexpected suspend count {0} for main thread with Hybrid injection", currentSuspendCount); break; default: + Log.Warning("Unknown LoadMethod {0}", info.LoadMethod); break; }