From 386e5f245c9661ebbd27f270952648852e4dabde Mon Sep 17 00:00:00 2001 From: marzent Date: Sun, 11 Feb 2024 22:01:34 +0100 Subject: [PATCH] Make the crash handler work on wine properly (#1636) * add LoadMethod to DalamudStartInfo * add to_wstring utility function * append full injector launch args for VEH * remove usage of std::chrono::zoned_time * fix injector arguments in crash handler restart * enable VEH on wine * remove dead wine detection code * write out tspack with std::fstream * fix off-by-one error in get_window_string() * remove usage of std::chrono when writing tspack * do not deadlock on crashing DalamudCrashHandler --- Dalamud.Boot/DalamudStartInfo.cpp | 15 +++ Dalamud.Boot/DalamudStartInfo.h | 7 ++ Dalamud.Boot/crashhandler_shared.h | 1 + Dalamud.Boot/dllmain.cpp | 2 - Dalamud.Boot/utils.cpp | 23 ++--- Dalamud.Boot/utils.h | 2 +- Dalamud.Boot/veh.cpp | 40 +++++++- Dalamud.Common/DalamudStartInfo.cs | 5 + Dalamud.Common/LoadMethod.cs | 17 ++++ Dalamud.Injector/EntryPoint.cs | 8 +- DalamudCrashHandler/DalamudCrashHandler.cpp | 105 ++++++++++---------- 11 files changed, 148 insertions(+), 77 deletions(-) create mode 100644 Dalamud.Common/LoadMethod.cs diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index e2fed1beb..d20265bf8 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -68,10 +68,25 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::ClientLanguage& val } } +void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) { + if (json.is_number_integer()) { + value = static_cast(json.get()); + + } + else if (json.is_string()) { + const auto langstr = unicode::convert(json.get(), &unicode::lower); + if (langstr == "entrypoint") + value = DalamudStartInfo::LoadMethod::Entrypoint; + else if (langstr == "inject") + value = DalamudStartInfo::LoadMethod::DllInject; + } +} + void from_json(const nlohmann::json& json, DalamudStartInfo& config) { if (!json.is_object()) return; + config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod); config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory); config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 73a1a0d34..5cee8f16b 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -26,6 +26,13 @@ struct DalamudStartInfo { }; friend void from_json(const nlohmann::json&, ClientLanguage&); + enum class LoadMethod : int { + Entrypoint, + DllInject, + }; + friend void from_json(const nlohmann::json&, LoadMethod&); + + LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint; std::string WorkingDirectory; std::string ConfigurationPath; std::string PluginDirectory; diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h index 4e8cbb520..8d93e4460 100644 --- a/Dalamud.Boot/crashhandler_shared.h +++ b/Dalamud.Boot/crashhandler_shared.h @@ -14,6 +14,7 @@ struct exception_info CONTEXT ContextRecord; uint64_t nLifetime; HANDLE hThreadHandle; + HANDLE hEventHandle; DWORD dwStackTraceLength; DWORD dwTroubleshootingPackDataLength; }; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 8ffef40b0..2566016e8 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -135,8 +135,6 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::I("Initializing VEH..."); if (g_startInfo.NoExceptionHandlers) { logging::W("=> Exception handlers are disabled from DalamudStartInfo."); - } else if (utils::is_running_on_wine()) { - logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) logging::I("=> Done!"); diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index b45795045..62a9d7055 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,21 +578,14 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } -bool utils::is_running_on_wine() { - if (get_env(L"XL_WINEONLINUX")) - return true; - HMODULE hntdll = GetModuleHandleW(L"ntdll.dll"); - if (!hntdll) - return true; - if (GetProcAddress(hntdll, "wine_get_version")) - return true; - if (GetProcAddress(hntdll, "wine_get_host_version")) - return true; - if (GetProcAddress(hntdll, "wine_server_call")) - return true; - if (GetProcAddress(hntdll, "wine_unix_to_nt_file_name")) - return true; - return false; +std::wstring utils::to_wstring(const std::string& str) { + if (str.empty()) return std::wstring(); + size_t convertedChars = 0; + size_t newStrSize = str.size() + 1; + std::wstring wstr(newStrSize, L'\0'); + mbstowcs_s(&convertedChars, &wstr[0], newStrSize, str.c_str(), _TRUNCATE); + wstr.resize(convertedChars - 1); + return wstr; } std::filesystem::path utils::get_module_path(HMODULE hModule) { diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index 5e3caa4d6..ebf48a294 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,7 +264,7 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } - bool is_running_on_wine(); + std::wstring to_wstring(const std::string& str); std::filesystem::path get_module_path(HMODULE hModule); diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 0898441ff..eb27acce7 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -26,6 +26,7 @@ PVOID g_veh_handle = nullptr; bool g_veh_do_full_dump = false; HANDLE g_crashhandler_process = nullptr; +HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_pipe_write = nullptr; std::recursive_mutex g_exception_handler_mutex; @@ -101,8 +102,21 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address) static void append_injector_launch_args(std::vector& args) { - args.emplace_back(L"-g"); - args.emplace_back(utils::loaded_module::current_process().path().wstring()); + args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\""); + switch (g_startInfo.DalamudLoadMethod) { + case DalamudStartInfo::LoadMethod::Entrypoint: + args.emplace_back(L"--mode=entrypoint"); + break; + case DalamudStartInfo::LoadMethod::DllInject: + args.emplace_back(L"--mode=inject"); + } + args.emplace_back(L"--logpath=\"" + utils::to_wstring(g_startInfo.BootLogPath) + L"\""); + args.emplace_back(L"--dalamud-working-directory=\"" + utils::to_wstring(g_startInfo.WorkingDirectory) + L"\""); + args.emplace_back(L"--dalamud-configuration-path=\"" + utils::to_wstring(g_startInfo.ConfigurationPath) + L"\""); + args.emplace_back(L"--dalamud-plugin-directory=\"" + utils::to_wstring(g_startInfo.PluginDirectory) + L"\""); + args.emplace_back(L"--dalamud-asset-directory=\"" + utils::to_wstring(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(L"--dalamud-client-language=" + std::to_wstring(static_cast(g_startInfo.Language))); + args.emplace_back(L"--dalamud-delay-initialize=" + std::to_wstring(g_startInfo.DelayInitializeMs)); if (g_startInfo.BootShowConsole) args.emplace_back(L"--console"); if (g_startInfo.BootEnableEtw) @@ -158,6 +172,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) g_time_start.time_since_epoch()).count(); exinfo.nLifetime = lifetime; DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), g_crashhandler_process, &exinfo.hThreadHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); + DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); std::wstring stackTrace; if (void* fn; const auto err = static_cast(g_clr->get_function_pointer( @@ -185,7 +200,20 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written) return EXCEPTION_CONTINUE_SEARCH; - SuspendThread(GetCurrentThread()); + HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event }; + DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + + switch (waitResult) { + case WAIT_OBJECT_0: + logging::E("DalamudCrashHandler.exe exited unexpectedly"); + break; + case WAIT_OBJECT_0 + 1: + logging::I("Crashing thread was resumed"); + break; + default: + logging::E("Unexpected WaitForMultipleObjects return code 0x{:x}", waitResult); + } + return EXCEPTION_CONTINUE_SEARCH; } @@ -308,6 +336,12 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) return false; } + if (!(g_crashhandler_event = CreateEventW(NULL, FALSE, FALSE, NULL))) + { + logging::W("Failed to create crash synchronization event: CreateEventW error 0x{:x}", GetLastError()); + return false; + } + CloseHandle(pi.hThread); g_crashhandler_process = pi.hProcess; diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index 5126fe3a4..edf21d174 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -17,6 +17,11 @@ public record DalamudStartInfo // ignored } + /// + /// Gets or sets the Dalamud load method. + /// + public LoadMethod LoadMethod { get; set; } + /// /// Gets or sets the working directory of the XIVLauncher installations. /// diff --git a/Dalamud.Common/LoadMethod.cs b/Dalamud.Common/LoadMethod.cs new file mode 100644 index 000000000..ca50098e2 --- /dev/null +++ b/Dalamud.Common/LoadMethod.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Common; + +/// +/// Enum describing the method Dalamud has been loaded. +/// +public enum LoadMethod +{ + /// + /// Load Dalamud by rewriting the games entrypoint. + /// + Entrypoint, + + /// + /// Load Dalamud via DLL-injection. + /// + DllInject, +} diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 3ffb7ba18..f839d9656 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -680,11 +680,11 @@ namespace Dalamud.Injector mode = mode == null ? "entrypoint" : mode.ToLowerInvariant(); if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode) { - mode = "entrypoint"; + dalamudStartInfo.LoadMethod = LoadMethod.Entrypoint; } else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode) { - mode = "inject"; + dalamudStartInfo.LoadMethod = LoadMethod.DllInject; } else { @@ -796,7 +796,7 @@ namespace Dalamud.Injector noFixAcl, p => { - if (!withoutDalamud && mode == "entrypoint") + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Entrypoint) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -813,7 +813,7 @@ namespace Dalamud.Injector Log.Verbose("Game process started with PID {0}", process.Id); - if (!withoutDalamud && mode == "inject") + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 18f7f0791..1930b6fb4 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -58,7 +58,7 @@ std::wstring u8_to_ws(const std::string& s) { } std::wstring get_window_string(HWND hWnd) { - std::wstring buf(GetWindowTextLengthW(hWnd), L'\0'); + std::wstring buf(GetWindowTextLengthW(hWnd) + 1, L'\0'); GetWindowTextW(hWnd, &buf[0], static_cast(buf.size())); return buf; } @@ -456,8 +456,10 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s { L"All files (*.*)", L"*" }, }}; - IShellItemPtr pItem; + std::optional filePath; + std::fstream fileStream; try { + IShellItemPtr pItem; SYSTEMTIME st; GetLocalTime(&st); IFileSaveDialogPtr pDialog; @@ -474,33 +476,39 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } throw_if_failed(pDialog->GetResult(&pItem), {}, "pDialog->GetResult"); - - IBindCtxPtr pBindCtx; - throw_if_failed(CreateBindCtx(0, &pBindCtx), {}, "CreateBindCtx"); - auto options = BIND_OPTS{.cbStruct = sizeof(BIND_OPTS), .grfMode = STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE}; - throw_if_failed(pBindCtx->SetBindOptions(&options), {}, "pBindCtx->SetBindOptions"); + PWSTR pFilePath = nullptr; + throw_if_failed(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pFilePath), {}, "pItem->GetDisplayName"); + pItem.Release(); + filePath.emplace(pFilePath); - IStreamPtr pStream; - throw_if_failed(pItem->BindToHandler(pBindCtx, BHID_Stream, IID_PPV_ARGS(&pStream)), {}, "pItem->BindToHandler"); - - throw_if_failed(pStream->SetSize({}), {}, "pStream->SetSize"); + fileStream.open(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); mz_zip_archive zipa{}; - zipa.m_pIO_opaque = &*pStream; + zipa.m_pIO_opaque = &fileStream; zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { - const auto pStream = static_cast(pOpaque); - throw_if_failed(pStream->Seek({ .QuadPart = static_cast(file_ofs) }, STREAM_SEEK_SET, nullptr), {}, "pStream->Seek"); - ULONG read; - throw_if_failed(pStream->Read(pBuf, static_cast(n), &read), {}, "pStream->Read"); - return read; + const auto pStream = static_cast(pOpaque); + if (!pStream || !pStream->is_open()) + throw std::runtime_error("Read operation failed: Stream is not open"); + pStream->seekg(file_ofs, std::ios::beg); + if (pStream->fail()) + throw std::runtime_error("Read operation failed: Error seeking in stream"); + pStream->read(static_cast(pBuf), n); + if (pStream->fail()) + throw std::runtime_error("Read operation failed: Error reading from stream"); + return pStream->gcount(); }; zipa.m_pWrite = [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n) -> size_t { - const auto pStream = static_cast(pOpaque); - throw_if_failed(pStream->Seek({ .QuadPart = static_cast(file_ofs) }, STREAM_SEEK_SET, nullptr), {}, "pStream->Seek"); - ULONG written; - throw_if_failed(pStream->Write(pBuf, static_cast(n), &written), {}, "pStream->Write"); - return written; + const auto pStream = static_cast(pOpaque); + if (!pStream || !pStream->is_open()) + throw std::runtime_error("Write operation failed: Stream is not open"); + pStream->seekp(file_ofs, std::ios::beg); + if (pStream->fail()) + throw std::runtime_error("Write operation failed: Error seeking in stream"); + pStream->write(static_cast(pBuf), n); + if (pStream->fail()) + throw std::runtime_error("Write operation failed: Error writing to stream"); + return n; }; const auto mz_throw_if_failed = [&zipa](mz_bool res, const std::string& clue) { if (!res) @@ -545,7 +553,14 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } auto handleInfo = HandleAndBaseOffset{.h = hLogFile, .off = baseOffset.QuadPart}; - const auto modt = std::chrono::system_clock::to_time_t(std::chrono::clock_cast(last_write_time(logFilePath))); + WIN32_FILE_ATTRIBUTE_DATA fileInfo = { 0 }; + time_t modt = time(nullptr); + if (GetFileAttributesExW(logFilePath.c_str(), GetFileExInfoStandard, &fileInfo)) { + ULARGE_INTEGER ull = { 0 }; + ull.LowPart = fileInfo.ftLastWriteTime.dwLowDateTime; + ull.HighPart = fileInfo.ftLastWriteTime.dwHighDateTime; + modt = ull.QuadPart / 10000000ULL - 11644473600ULL; + } mz_throw_if_failed(mz_zip_writer_add_read_buf_callback( &zipa, pcszLogFileName, @@ -564,34 +579,20 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } catch (const std::exception& e) { MessageBoxW(hWndParent, std::format(L"Failed to save file: {}", u8_to_ws(e.what())).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONERROR); - - if (pItem) { + fileStream.close(); + if (filePath) { try { - IFileOperationPtr pFileOps; - throw_if_failed(pFileOps.CreateInstance(__uuidof(FileOperation), nullptr, CLSCTX_ALL)); - throw_if_failed(pFileOps->SetOperationFlags(FOF_NO_UI)); - throw_if_failed(pFileOps->DeleteItem(pItem, nullptr)); - throw_if_failed(pFileOps->PerformOperations()); - } catch (const std::exception& e2) { + std::filesystem::remove(*filePath); + } catch (const std::filesystem::filesystem_error& e2) { std::wcerr << std::format(L"Failed to remove temporary file: {}", u8_to_ws(e2.what())) << std::endl; } - pItem.Release(); } + return; } - if (pItem) { - PWSTR pwszFileName; - if (FAILED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pwszFileName))) { - if (FAILED(pItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEEDITING, &pwszFileName))) { - MessageBoxW(hWndParent, L"The file has been saved to the specified path.", get_window_string(hWndParent).c_str(), MB_OK | MB_ICONINFORMATION); - } else { - std::unique_ptr::type, decltype(CoTaskMemFree)*> pszFileNamePtr(pwszFileName, CoTaskMemFree); - MessageBoxW(hWndParent, std::format(L"The file has been saved to: {}", pwszFileName).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONINFORMATION); - } - } else { - std::unique_ptr::type, decltype(CoTaskMemFree)*> pszFileNamePtr(pwszFileName, CoTaskMemFree); - ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", pwszFileName)).c_str(), nullptr, SW_SHOW); - } + fileStream.close(); + if (filePath) { + ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", *filePath)).c_str(), nullptr, SW_SHOW); } } @@ -612,7 +613,8 @@ void restart_game_using_injector(int nRadioButton, const std::vector args; - args.emplace_back((std::filesystem::path(pathStr).parent_path() / L"Dalamud.Injector.exe").wstring()); + std::wstring injectorPath = (std::filesystem::path(pathStr).parent_path() / L"Dalamud.Injector.exe").wstring(); + args.emplace_back(L'\"' + injectorPath + L'\"'); args.emplace_back(L"launch"); switch (nRadioButton) { case IdRadioRestartWithout3pPlugins: @@ -625,12 +627,11 @@ void restart_game_using_injector(int nRadioButton, const std::vector