From f958cbc737542332ed4de9cf503fa4a8d1106564 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 11:29:10 -0400 Subject: [PATCH 1/7] yuzu: Use a debugger to generate minidumps yuzu: Move mini_dump out of core startup_checks: Better exception handling --- src/common/settings.h | 1 + src/yuzu/CMakeLists.txt | 10 + src/yuzu/applets/qt_controller.cpp | 2 +- src/yuzu/configuration/config.cpp | 6 +- src/yuzu/configuration/config.h | 4 +- src/yuzu/configuration/configure_debug.cpp | 21 +- src/yuzu/configuration/configure_debug.h | 2 + src/yuzu/configuration/configure_debug.ui | 122 ++++++------ src/yuzu/configuration/configure_input.cpp | 2 +- src/yuzu/configuration/configure_per_game.cpp | 3 +- src/yuzu/configuration/input_profiles.cpp | 9 +- src/yuzu/configuration/input_profiles.h | 4 +- src/yuzu/main.cpp | 32 ++- src/yuzu/main.h | 2 +- src/yuzu/mini_dump.cpp | 185 ++++++++++++++++++ src/yuzu/mini_dump.h | 12 ++ src/yuzu/startup_checks.cpp | 29 ++- src/yuzu/startup_checks.h | 5 +- 18 files changed, 360 insertions(+), 91 deletions(-) create mode 100644 src/yuzu/mini_dump.cpp create mode 100644 src/yuzu/mini_dump.h diff --git a/src/common/settings.h b/src/common/settings.h index 14ed9b237..8354fdba7 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -529,6 +529,7 @@ struct Values { Setting use_debug_asserts{false, "use_debug_asserts"}; Setting use_auto_stub{false, "use_auto_stub"}; Setting enable_all_controllers{false, "enable_all_controllers"}; + Setting create_crash_dumps{false, "create_crash_dumps"}; // Miscellaneous Setting log_filter{"*:Info", "log_filter"}; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 50007338f..3d9906ade 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -208,6 +208,16 @@ add_executable(yuzu yuzu.rc ) +if (WIN32 AND NOT ("${DBGHELP_LIBRARY}" STREQUAL "DBGHELP_LIBRARY-NOTFOUND")) + target_sources(yuzu PRIVATE + mini_dump.cpp + mini_dump.h + ) + + target_link_libraries(yuzu PUBLIC ${DBGHELP_LIBRARY}) + target_compile_definitions(yuzu PRIVATE -DYUZU_DBGHELP) +endif() + file(GLOB COMPAT_LIST ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) diff --git a/src/yuzu/applets/qt_controller.cpp b/src/yuzu/applets/qt_controller.cpp index 8be311fcb..1d8072243 100644 --- a/src/yuzu/applets/qt_controller.cpp +++ b/src/yuzu/applets/qt_controller.cpp @@ -63,7 +63,7 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( InputCommon::InputSubsystem* input_subsystem_, Core::System& system_) : QDialog(parent), ui(std::make_unique()), parameters(std::move(parameters_)), input_subsystem{input_subsystem_}, - input_profiles(std::make_unique(system_)), system{system_} { + input_profiles(std::make_unique()), system{system_} { ui->setupUi(this); player_widgets = { diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index da6e5aa88..e44759856 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -15,8 +15,7 @@ namespace FS = Common::FS; -Config::Config(Core::System& system_, const std::string& config_name, ConfigType config_type) - : type(config_type), system{system_} { +Config::Config(const std::string& config_name, ConfigType config_type) : type(config_type) { global = config_type == ConfigType::GlobalConfig; Initialize(config_name); @@ -546,6 +545,7 @@ void Config::ReadDebuggingValues() { ReadBasicSetting(Settings::values.use_debug_asserts); ReadBasicSetting(Settings::values.use_auto_stub); ReadBasicSetting(Settings::values.enable_all_controllers); + ReadBasicSetting(Settings::values.create_crash_dumps); qt_config->endGroup(); } @@ -1160,6 +1160,7 @@ void Config::SaveDebuggingValues() { WriteBasicSetting(Settings::values.use_debug_asserts); WriteBasicSetting(Settings::values.disable_macro_jit); WriteBasicSetting(Settings::values.enable_all_controllers); + WriteBasicSetting(Settings::values.create_crash_dumps); qt_config->endGroup(); } @@ -1545,7 +1546,6 @@ void Config::Reload() { ReadValues(); // To apply default value changes SaveValues(); - system.ApplySettings(); } void Config::Save() { diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 486ceea94..06fa7d2d0 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -25,7 +25,7 @@ public: InputProfile, }; - explicit Config(Core::System& system_, const std::string& config_name = "qt-config", + explicit Config(const std::string& config_name = "qt-config", ConfigType config_type = ConfigType::GlobalConfig); ~Config(); @@ -194,8 +194,6 @@ private: std::unique_ptr qt_config; std::string qt_config_loc; bool global; - - Core::System& system; }; // These metatype declarations cannot be in common/settings.h because core is devoid of QT diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 04d397750..622808e94 100644 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include "common/fs/path_util.h" #include "common/logging/backend.h" @@ -26,6 +27,16 @@ ConfigureDebug::ConfigureDebug(const Core::System& system_, QWidget* parent) connect(ui->toggle_gdbstub, &QCheckBox::toggled, [&]() { ui->gdbport_spinbox->setEnabled(ui->toggle_gdbstub->isChecked()); }); + + connect(ui->create_crash_dumps, &QCheckBox::stateChanged, [&](int) { + if (crash_dump_warning_shown) { + return; + } + QMessageBox::warning(this, tr("Restart Required"), + tr("yuzu is required to restart in order to apply this setting."), + QMessageBox::Ok, QMessageBox::Ok); + crash_dump_warning_shown = true; + }); } ConfigureDebug::~ConfigureDebug() = default; @@ -71,7 +82,14 @@ void ConfigureDebug::SetConfiguration() { ui->disable_web_applet->setChecked(UISettings::values.disable_web_applet.GetValue()); #else ui->disable_web_applet->setEnabled(false); - ui->disable_web_applet->setText(QString::fromUtf8("Web applet not compiled")); + ui->disable_web_applet->setText(tr("Web applet not compiled")); +#endif + +#ifdef YUZU_DBGHELP + ui->create_crash_dumps->setChecked(Settings::values.create_crash_dumps.GetValue()); +#else + ui->create_crash_dumps->setEnabled(false); + ui->create_crash_dumps->setText(tr("MiniDump creation not compiled")); #endif } @@ -84,6 +102,7 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked(); Settings::values.reporting_services = ui->reporting_services->isChecked(); Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked(); + Settings::values.create_crash_dumps = ui->create_crash_dumps->isChecked(); Settings::values.quest_flag = ui->quest_flag->isChecked(); Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); Settings::values.use_auto_stub = ui->use_auto_stub->isChecked(); diff --git a/src/yuzu/configuration/configure_debug.h b/src/yuzu/configuration/configure_debug.h index 42d30f170..030a0b7f7 100644 --- a/src/yuzu/configuration/configure_debug.h +++ b/src/yuzu/configuration/configure_debug.h @@ -32,4 +32,6 @@ private: std::unique_ptr ui; const Core::System& system; + + bool crash_dump_warning_shown{false}; }; diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 47b8b80f1..314d47af5 100644 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -7,60 +7,60 @@ - - - - - - Debugger - - + + + + + + Debugger + + + + - - - - - Enable GDB Stub - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Port: - - - - - - - 1024 - - - 65535 - - - - + + + Enable GDB Stub + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Port: + + + + + + + 1024 + + + 65535 + + - - - + + + + + @@ -231,6 +231,13 @@ Debugging + + + + Enable Verbose Reporting Services** + + + @@ -238,20 +245,20 @@ - + - - Dump Audio Commands To Console** - Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer. + + Dump Audio Commands To Console** + - - + + - Enable Verbose Reporting Services** + Create Minidump After Crash @@ -340,7 +347,6 @@ disable_loop_safety_checks fs_access_log reporting_services - dump_audio_commands quest_flag enable_cpu_debugging use_debug_asserts diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp index 16fba3deb..cb55472c9 100644 --- a/src/yuzu/configuration/configure_input.cpp +++ b/src/yuzu/configuration/configure_input.cpp @@ -65,7 +65,7 @@ void OnDockedModeChanged(bool last_state, bool new_state, Core::System& system) ConfigureInput::ConfigureInput(Core::System& system_, QWidget* parent) : QWidget(parent), ui(std::make_unique()), - profiles(std::make_unique(system_)), system{system_} { + profiles(std::make_unique()), system{system_} { ui->setupUi(this); } diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp index af8343b2e..c3cb8f61d 100644 --- a/src/yuzu/configuration/configure_per_game.cpp +++ b/src/yuzu/configuration/configure_per_game.cpp @@ -42,8 +42,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st const auto file_path = std::filesystem::path(Common::FS::ToU8String(file_name)); const auto config_file_name = title_id == 0 ? Common::FS::PathToUTF8String(file_path.filename()) : fmt::format("{:016X}", title_id); - game_config = - std::make_unique(system, config_file_name, Config::ConfigType::PerGameConfig); + game_config = std::make_unique(config_file_name, Config::ConfigType::PerGameConfig); addons_tab = std::make_unique(system_, this); audio_tab = std::make_unique(system_, this); diff --git a/src/yuzu/configuration/input_profiles.cpp b/src/yuzu/configuration/input_profiles.cpp index 20b22e7de..807afbeb2 100644 --- a/src/yuzu/configuration/input_profiles.cpp +++ b/src/yuzu/configuration/input_profiles.cpp @@ -27,7 +27,7 @@ std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) { } // namespace -InputProfiles::InputProfiles(Core::System& system_) : system{system_} { +InputProfiles::InputProfiles() { const auto input_profile_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "input"; if (!FS::IsDir(input_profile_loc)) { @@ -43,8 +43,8 @@ InputProfiles::InputProfiles(Core::System& system_) : system{system_} { if (IsINI(filename) && IsProfileNameValid(name_without_ext)) { map_profiles.insert_or_assign( - name_without_ext, std::make_unique(system, name_without_ext, - Config::ConfigType::InputProfile)); + name_without_ext, + std::make_unique(name_without_ext, Config::ConfigType::InputProfile)); } return true; @@ -80,8 +80,7 @@ bool InputProfiles::CreateProfile(const std::string& profile_name, std::size_t p } map_profiles.insert_or_assign( - profile_name, - std::make_unique(system, profile_name, Config::ConfigType::InputProfile)); + profile_name, std::make_unique(profile_name, Config::ConfigType::InputProfile)); return SaveProfile(profile_name, player_index); } diff --git a/src/yuzu/configuration/input_profiles.h b/src/yuzu/configuration/input_profiles.h index 65fc9e62c..2bf3e4250 100644 --- a/src/yuzu/configuration/input_profiles.h +++ b/src/yuzu/configuration/input_profiles.h @@ -15,7 +15,7 @@ class Config; class InputProfiles { public: - explicit InputProfiles(Core::System& system_); + explicit InputProfiles(); virtual ~InputProfiles(); std::vector GetInputProfileNames(); @@ -31,6 +31,4 @@ private: bool ProfileExistsInMap(const std::string& profile_name) const; std::unordered_map> map_profiles; - - Core::System& system; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index a85adc072..ca3f4da70 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -138,6 +138,10 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/uisettings.h" #include "yuzu/util/clickable_label.h" +#ifdef YUZU_DBGHELP +#include "yuzu/mini_dump.h" +#endif + using namespace Common::Literals; #ifdef USE_DISCORD_PRESENCE @@ -269,10 +273,9 @@ bool GMainWindow::CheckDarkMode() { #endif // __linux__ } -GMainWindow::GMainWindow(bool has_broken_vulkan) +GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulkan) : ui{std::make_unique()}, system{std::make_unique()}, - input_subsystem{std::make_shared()}, - config{std::make_unique(*system)}, + input_subsystem{std::make_shared()}, config{std::move(config_)}, vfs{std::make_shared()}, provider{std::make_unique()} { #ifdef __linux__ @@ -1637,7 +1640,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t const auto config_file_name = title_id == 0 ? Common::FS::PathToUTF8String(file_path.filename()) : fmt::format("{:016X}", title_id); - Config per_game_config(*system, config_file_name, Config::ConfigType::PerGameConfig); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + system->ApplySettings(); } // Save configurations @@ -2981,7 +2985,7 @@ void GMainWindow::OnConfigure() { Settings::values.disabled_addons.clear(); - config = std::make_unique(*system); + config = std::make_unique(); UISettings::values.reset_to_defaults = false; UISettings::values.game_dirs = std::move(old_game_dirs); @@ -3042,6 +3046,7 @@ void GMainWindow::OnConfigure() { UpdateStatusButtons(); controller_dialog->refreshConfiguration(); + system->ApplySettings(); } void GMainWindow::OnConfigureTas() { @@ -4082,7 +4087,22 @@ void GMainWindow::changeEvent(QEvent* event) { #endif int main(int argc, char* argv[]) { + std::unique_ptr config = std::make_unique(); bool has_broken_vulkan = false; + bool is_child = false; + if (CheckEnvVars(&is_child)) { + return 0; + } + +#ifdef YUZU_DBGHELP + PROCESS_INFORMATION pi; + if (!is_child && Settings::values.create_crash_dumps.GetValue() && SpawnDebuggee(argv[0], pi)) { + config.reset(nullptr); + DebugDebuggee(pi); + return 0; + } +#endif + if (StartupChecks(argv[0], &has_broken_vulkan)) { return 0; } @@ -4135,7 +4155,7 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - GMainWindow main_window{has_broken_vulkan}; + GMainWindow main_window{std::move(config), has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 1ae2b93d9..716aef063 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -120,7 +120,7 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit GMainWindow(bool has_broken_vulkan); + explicit GMainWindow(std::unique_ptr config_, bool has_broken_vulkan); ~GMainWindow() override; bool DropAction(QDropEvent* event); diff --git a/src/yuzu/mini_dump.cpp b/src/yuzu/mini_dump.cpp new file mode 100644 index 000000000..ad8a4f607 --- /dev/null +++ b/src/yuzu/mini_dump.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include "common/logging/log.h" +#include "yuzu/mini_dump.h" +#include "yuzu/startup_checks.h" + +// dbghelp.h must be included after windows.h +#include + +void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, + EXCEPTION_POINTERS* pep) { + LOG_INFO(Core, "called"); + + char file_name[255]; + const std::time_t the_time = std::time(nullptr); + std::strftime(file_name, 255, "yuzu-crash-%Y%m%d%H%M%S.dmp", std::localtime(&the_time)); + + // Open the file + HANDLE file_handle = CreateFile(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if ((file_handle != nullptr) && (file_handle != INVALID_HANDLE_VALUE)) { + // Create the minidump + const MINIDUMP_TYPE dump_type = MiniDumpNormal; + + const bool write_dump_status = MiniDumpWriteDump(process_handle, process_id, file_handle, + dump_type, (pep != 0) ? info : 0, 0, 0); + + if (!write_dump_status) { + LOG_ERROR(Core, "MiniDumpWriteDump failed. Error: {}", GetLastError()); + } else { + LOG_INFO(Core, "Minidump created."); + } + + // Close the file + CloseHandle(file_handle); + + } else { + LOG_ERROR(Core, "CreateFile failed. Error: {}", GetLastError()); + } +} + +bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { + std::memset(&pi, 0, sizeof(pi)); + + if (!SpawnChild(arg0, &pi, 0)) { + std::fprintf(stderr, "warning: continuing without crash dumps\n"); + return false; + } + + // Don't debug if we are already being debugged + if (IsDebuggerPresent()) { + return false; + } + + const bool can_debug = DebugActiveProcess(pi.dwProcessId); + if (!can_debug) { + std::fprintf(stderr, + "warning: DebugActiveProcess failed (%d), continuing without crash dumps\n", + GetLastError()); + return false; + } + + return true; +} + +void DebugDebuggee(PROCESS_INFORMATION& pi) { + DEBUG_EVENT deb_ev; + + while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { + const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); + if (!wait_success) { + std::fprintf(stderr, "error: WaitForDebugEvent failed (%d)\n", GetLastError()); + return; + } + + switch (deb_ev.dwDebugEventCode) { + case OUTPUT_DEBUG_STRING_EVENT: + case CREATE_PROCESS_DEBUG_EVENT: + case CREATE_THREAD_DEBUG_EVENT: + case EXIT_PROCESS_DEBUG_EVENT: + case EXIT_THREAD_DEBUG_EVENT: + case LOAD_DLL_DEBUG_EVENT: + case RIP_EVENT: + case UNLOAD_DLL_DEBUG_EVENT: + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); + break; + case EXCEPTION_DEBUG_EVENT: + EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + + std::fprintf(stderr, "ExceptionCode: 0x%08x %s\n", record.ExceptionCode, + ExceptionName(record.ExceptionCode)); + if (!deb_ev.u.Exception.dwFirstChance) { + HANDLE thread_handle = OpenThread(THREAD_ALL_ACCESS, false, deb_ev.dwThreadId); + if (thread_handle == nullptr) { + std::fprintf(stderr, "OpenThread failed (%d)\n", GetLastError()); + } + if (SuspendThread(thread_handle) == (DWORD)-1) { + std::fprintf(stderr, "SuspendThread failed (%d)\n", GetLastError()); + } + + CONTEXT context; + std::memset(&context, 0, sizeof(context)); + context.ContextFlags = CONTEXT_ALL; + if (!GetThreadContext(thread_handle, &context)) { + std::fprintf(stderr, "GetThreadContext failed (%d)\n", GetLastError()); + break; + } + + EXCEPTION_POINTERS ep; + ep.ExceptionRecord = &record; + ep.ContextRecord = &context; + + MINIDUMP_EXCEPTION_INFORMATION info; + info.ThreadId = deb_ev.dwThreadId; + info.ExceptionPointers = &ep; + info.ClientPointers = false; + + CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep); + + std::fprintf(stderr, "previous thread suspend count: %d\n", + ResumeThread(thread_handle)); + if (CloseHandle(thread_handle) == 0) { + std::fprintf(stderr, "error: CloseHandle(thread_handle) failed (%d)\n", + GetLastError()); + } + } + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); + break; + } + } +} + +const char* ExceptionName(DWORD exception) { + switch (exception) { + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + case EXCEPTION_BREAKPOINT: + return "EXCEPTION_BREAKPOINT"; + case EXCEPTION_SINGLE_STEP: + return "EXCEPTION_SINGLE_STEP"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "EXCEPTION_FLT_DENORMAL_OPERAND"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_INEXACT_RESULT: + return "EXCEPTION_FLT_INEXACT_RESULT"; + case EXCEPTION_FLT_INVALID_OPERATION: + return "EXCEPTION_FLT_INVALID_OPERATION"; + case EXCEPTION_FLT_OVERFLOW: + return "EXCEPTION_FLT_OVERFLOW"; + case EXCEPTION_FLT_STACK_CHECK: + return "EXCEPTION_FLT_STACK_CHECK"; + case EXCEPTION_FLT_UNDERFLOW: + return "EXCEPTION_FLT_UNDERFLOW"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: + return "EXCEPTION_INT_OVERFLOW"; + case EXCEPTION_PRIV_INSTRUCTION: + return "EXCEPTION_PRIV_INSTRUCTION"; + case EXCEPTION_IN_PAGE_ERROR: + return "EXCEPTION_IN_PAGE_ERROR"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + case EXCEPTION_INVALID_DISPOSITION: + return "EXCEPTION_INVALID_DISPOSITION"; + case EXCEPTION_GUARD_PAGE: + return "EXCEPTION_GUARD_PAGE"; + case EXCEPTION_INVALID_HANDLE: + return "EXCEPTION_INVALID_HANDLE"; + default: + return nullptr; + } +} diff --git a/src/yuzu/mini_dump.h b/src/yuzu/mini_dump.h new file mode 100644 index 000000000..59ba615e4 --- /dev/null +++ b/src/yuzu/mini_dump.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include + +void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, + EXCEPTION_POINTERS* pep); + +bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi); +void DebugDebuggee(PROCESS_INFORMATION& pi); +const char* ExceptionName(DWORD exception); diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 8421280bf..29b87da05 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -31,19 +31,36 @@ void CheckVulkan() { } } -bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { +bool CheckEnvVars(bool* is_child) { #ifdef _WIN32 // Check environment variable to see if we are the child char variable_contents[8]; const DWORD startup_check_var = GetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, variable_contents, 8); - if (startup_check_var > 0 && std::strncmp(variable_contents, "ON", 8) == 0) { + if (startup_check_var > 0 && std::strncmp(variable_contents, ENV_VAR_ENABLED_TEXT, 8) == 0) { CheckVulkan(); return true; } + // Don't perform startup checks if we are a child process + char is_child_s[8]; + const DWORD is_child_len = GetEnvironmentVariableA(IS_CHILD_ENV_VAR, is_child_s, 8); + if (is_child_len > 0 && std::strncmp(is_child_s, ENV_VAR_ENABLED_TEXT, 8) == 0) { + *is_child = true; + return false; + } else if (!SetEnvironmentVariableA(IS_CHILD_ENV_VAR, ENV_VAR_ENABLED_TEXT)) { + std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", + IS_CHILD_ENV_VAR, GetLastError()); + return true; + } +#endif + return false; +} + +bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { +#ifdef _WIN32 // Set the startup variable for child processes - const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); + const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, ENV_VAR_ENABLED_TEXT); if (!env_var_set) { std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", STARTUP_CHECK_ENV_VAR, GetLastError()); @@ -53,7 +70,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { PROCESS_INFORMATION process_info; std::memset(&process_info, '\0', sizeof(process_info)); - if (!SpawnChild(arg0, &process_info)) { + if (!SpawnChild(arg0, &process_info, 0)) { return false; } @@ -106,7 +123,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { } #ifdef _WIN32 -bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags) { STARTUPINFOA startup_info; std::memset(&startup_info, '\0', sizeof(startup_info)); @@ -120,7 +137,7 @@ bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes false, // bInheritHandles - 0, // dwCreationFlags + flags, // dwCreationFlags nullptr, // lpEnvironment nullptr, // lpCurrentDirectory &startup_info, // lpStartupInfo diff --git a/src/yuzu/startup_checks.h b/src/yuzu/startup_checks.h index 096dd54a8..f2fc2d9d4 100644 --- a/src/yuzu/startup_checks.h +++ b/src/yuzu/startup_checks.h @@ -7,11 +7,14 @@ #include #endif +constexpr char IS_CHILD_ENV_VAR[] = "YUZU_IS_CHILD"; constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS"; +constexpr char ENV_VAR_ENABLED_TEXT[] = "ON"; void CheckVulkan(); +bool CheckEnvVars(bool* is_child); bool StartupChecks(const char* arg0, bool* has_broken_vulkan); #ifdef _WIN32 -bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi); +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags); #endif From 3dbaafe1f3364db2721a3318e0cde66fa0c81a5e Mon Sep 17 00:00:00 2001 From: lat9nq Date: Wed, 13 Jul 2022 12:14:48 -0400 Subject: [PATCH 2/7] mini_dump: Cleanup and add comments Removes some unnecessary code. wip --- src/yuzu/main.cpp | 1 + src/yuzu/mini_dump.cpp | 126 +++++++++++++++++++++++++++-------------- src/yuzu/mini_dump.h | 1 + 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ca3f4da70..ff59c64c3 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -4097,6 +4097,7 @@ int main(int argc, char* argv[]) { #ifdef YUZU_DBGHELP PROCESS_INFORMATION pi; if (!is_child && Settings::values.create_crash_dumps.GetValue() && SpawnDebuggee(argv[0], pi)) { + // Delete the config object so that it doesn't save when the program exits config.reset(nullptr); DebugDebuggee(pi); return 0; diff --git a/src/yuzu/mini_dump.cpp b/src/yuzu/mini_dump.cpp index ad8a4f607..e60456d9b 100644 --- a/src/yuzu/mini_dump.cpp +++ b/src/yuzu/mini_dump.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include "common/logging/log.h" #include "yuzu/mini_dump.h" #include "yuzu/startup_checks.h" @@ -11,8 +11,6 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, EXCEPTION_POINTERS* pep) { - LOG_INFO(Core, "called"); - char file_name[255]; const std::time_t the_time = std::time(nullptr); std::strftime(file_name, 255, "yuzu-crash-%Y%m%d%H%M%S.dmp", std::localtime(&the_time)); @@ -29,16 +27,50 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_ dump_type, (pep != 0) ? info : 0, 0, 0); if (!write_dump_status) { - LOG_ERROR(Core, "MiniDumpWriteDump failed. Error: {}", GetLastError()); + std::fprintf(stderr, "MiniDumpWriteDump failed. Error: %d\n", GetLastError()); } else { - LOG_INFO(Core, "Minidump created."); + std::fprintf(stderr, "MiniDump created: %s\n", file_name); } // Close the file CloseHandle(file_handle); } else { - LOG_ERROR(Core, "CreateFile failed. Error: {}", GetLastError()); + std::fprintf(stderr, "CreateFile failed. Error: %d\n", GetLastError()); + } +} + +void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { + EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + + HANDLE thread_handle = OpenThread(THREAD_GET_CONTEXT, false, deb_ev.dwThreadId); + if (thread_handle == nullptr) { + std::fprintf(stderr, "OpenThread failed (%d)\n", GetLastError()); + } + + // Get child process context + CONTEXT context; + std::memset(&context, 0, sizeof(context)); + context.ContextFlags = CONTEXT_ALL; + if (!GetThreadContext(thread_handle, &context)) { + std::fprintf(stderr, "GetThreadContext failed (%d)\n", GetLastError()); + return; + } + + // Create exception pointers for minidump + EXCEPTION_POINTERS ep; + ep.ExceptionRecord = &record; + ep.ContextRecord = &context; + + MINIDUMP_EXCEPTION_INFORMATION info; + info.ThreadId = deb_ev.dwThreadId; + info.ExceptionPointers = &ep; + info.ClientPointers = false; + + CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep); + + if (CloseHandle(thread_handle) == 0) { + std::fprintf(stderr, "error: CloseHandle(thread_handle) failed (%d)\n", GetLastError()); } } @@ -68,6 +100,8 @@ bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { void DebugDebuggee(PROCESS_INFORMATION& pi) { DEBUG_EVENT deb_ev; + const std::time_t start_time = std::time(nullptr); + //~ bool seen_nonzero_thread_exit = false; while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); @@ -87,49 +121,57 @@ void DebugDebuggee(PROCESS_INFORMATION& pi) { case UNLOAD_DLL_DEBUG_EVENT: ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); break; - case EXCEPTION_DEBUG_EVENT: + //~ case EXIT_THREAD_DEBUG_EVENT: { + //~ const DWORD& exit_code = deb_ev.u.ExitThread.dwExitCode; + + //~ // Generate a crash dump on the first abnormal thread exit. + //~ // We don't want to generate on every abnormal thread exit since ALL the other + // threads ~ // in the application will follow by exiting with the same code. ~ if + //(!seen_nonzero_thread_exit && exit_code != 0) { ~ seen_nonzero_thread_exit = true; ~ + // std::fprintf(stderr, ~ "Creating MiniDump on first non-zero thread exit: code + // 0x%08x\n", ~ exit_code); + //~ DumpFromDebugEvent(deb_ev, pi); + //~ } + //~ ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); + //~ break; + //~ } + case EXCEPTION_DEBUG_EVENT: { EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + const std::time_t now = std::time(nullptr); + const std::time_t delta = now - start_time; - std::fprintf(stderr, "ExceptionCode: 0x%08x %s\n", record.ExceptionCode, - ExceptionName(record.ExceptionCode)); + if (ExceptionName(record.ExceptionCode) == nullptr) { + int record_count = 0; + EXCEPTION_RECORD* next_record = &deb_ev.u.Exception.ExceptionRecord; + while (next_record != nullptr) { + std::fprintf(stderr, + "[%d] code(%d): 0x%08x\n\tflags: %08x %s\n\taddress: " + "0x%08x\n\tparameters: %d\n", + delta, record_count, next_record->ExceptionCode, + next_record->ExceptionFlags, + next_record->ExceptionFlags == EXCEPTION_NONCONTINUABLE + ? "noncontinuable" + : "", + next_record->ExceptionAddress, next_record->NumberParameters); + for (int i = 0; i < static_cast(next_record->NumberParameters); i++) { + std::fprintf(stderr, "\t\t%0d: 0x%08x\n", i, + next_record->ExceptionInformation[i]); + } + + record_count++; + next_record = next_record->ExceptionRecord; + } + } + // We want to generate a crash dump if we are seeing the same exception again. if (!deb_ev.u.Exception.dwFirstChance) { - HANDLE thread_handle = OpenThread(THREAD_ALL_ACCESS, false, deb_ev.dwThreadId); - if (thread_handle == nullptr) { - std::fprintf(stderr, "OpenThread failed (%d)\n", GetLastError()); - } - if (SuspendThread(thread_handle) == (DWORD)-1) { - std::fprintf(stderr, "SuspendThread failed (%d)\n", GetLastError()); - } - - CONTEXT context; - std::memset(&context, 0, sizeof(context)); - context.ContextFlags = CONTEXT_ALL; - if (!GetThreadContext(thread_handle, &context)) { - std::fprintf(stderr, "GetThreadContext failed (%d)\n", GetLastError()); - break; - } - - EXCEPTION_POINTERS ep; - ep.ExceptionRecord = &record; - ep.ContextRecord = &context; - - MINIDUMP_EXCEPTION_INFORMATION info; - info.ThreadId = deb_ev.dwThreadId; - info.ExceptionPointers = &ep; - info.ClientPointers = false; - - CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep); - - std::fprintf(stderr, "previous thread suspend count: %d\n", - ResumeThread(thread_handle)); - if (CloseHandle(thread_handle) == 0) { - std::fprintf(stderr, "error: CloseHandle(thread_handle) failed (%d)\n", - GetLastError()); - } + std::fprintf(stderr, "Creating MiniDump on ExceptionCode: 0x%08x %s\n", + record.ExceptionCode, ExceptionName(record.ExceptionCode)); + DumpFromDebugEvent(deb_ev, pi); } ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); break; } + } } } diff --git a/src/yuzu/mini_dump.h b/src/yuzu/mini_dump.h index 59ba615e4..f6f5dc2c7 100644 --- a/src/yuzu/mini_dump.h +++ b/src/yuzu/mini_dump.h @@ -7,6 +7,7 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, EXCEPTION_POINTERS* pep); +void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi); bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi); void DebugDebuggee(PROCESS_INFORMATION& pi); const char* ExceptionName(DWORD exception); From e339ec0e00905821dbb4fee8e45a2514555f5b0e Mon Sep 17 00:00:00 2001 From: lat9nq Date: Mon, 18 Jul 2022 21:36:26 -0400 Subject: [PATCH 3/7] mini_dump: Check for debugger before spawning a child mini_dump: Clean up mini_dump: Fix MSVC error mini_dump: Silence MSVC warning C4700 Zero initialize deb_ev. mini_dump: Add license info --- src/yuzu/mini_dump.cpp | 103 +++++++++++++++-------------------------- src/yuzu/mini_dump.h | 3 ++ 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/src/yuzu/mini_dump.cpp b/src/yuzu/mini_dump.cpp index e60456d9b..b25067c10 100644 --- a/src/yuzu/mini_dump.cpp +++ b/src/yuzu/mini_dump.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #include #include #include @@ -16,28 +19,28 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_ std::strftime(file_name, 255, "yuzu-crash-%Y%m%d%H%M%S.dmp", std::localtime(&the_time)); // Open the file - HANDLE file_handle = CreateFile(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + HANDLE file_handle = CreateFileA(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if ((file_handle != nullptr) && (file_handle != INVALID_HANDLE_VALUE)) { - // Create the minidump - const MINIDUMP_TYPE dump_type = MiniDumpNormal; - - const bool write_dump_status = MiniDumpWriteDump(process_handle, process_id, file_handle, - dump_type, (pep != 0) ? info : 0, 0, 0); - - if (!write_dump_status) { - std::fprintf(stderr, "MiniDumpWriteDump failed. Error: %d\n", GetLastError()); - } else { - std::fprintf(stderr, "MiniDump created: %s\n", file_name); - } - - // Close the file - CloseHandle(file_handle); - - } else { - std::fprintf(stderr, "CreateFile failed. Error: %d\n", GetLastError()); + if (file_handle == nullptr || file_handle == INVALID_HANDLE_VALUE) { + std::fprintf(stderr, "CreateFileA failed. Error: %d\n", GetLastError()); + return; } + + // Create the minidump + const MINIDUMP_TYPE dump_type = MiniDumpNormal; + + const bool write_dump_status = MiniDumpWriteDump(process_handle, process_id, file_handle, + dump_type, (pep != 0) ? info : 0, 0, 0); + + if (write_dump_status) { + std::fprintf(stderr, "MiniDump created: %s\n", file_name); + } else { + std::fprintf(stderr, "MiniDumpWriteDump failed. Error: %d\n", GetLastError()); + } + + // Close the file + CloseHandle(file_handle); } void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { @@ -77,13 +80,13 @@ void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { std::memset(&pi, 0, sizeof(pi)); - if (!SpawnChild(arg0, &pi, 0)) { - std::fprintf(stderr, "warning: continuing without crash dumps\n"); + // Don't debug if we are already being debugged + if (IsDebuggerPresent()) { return false; } - // Don't debug if we are already being debugged - if (IsDebuggerPresent()) { + if (!SpawnChild(arg0, &pi, 0)) { + std::fprintf(stderr, "warning: continuing without crash dumps\n"); return false; } @@ -100,8 +103,7 @@ bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { void DebugDebuggee(PROCESS_INFORMATION& pi) { DEBUG_EVENT deb_ev; - const std::time_t start_time = std::time(nullptr); - //~ bool seen_nonzero_thread_exit = false; + std::memset(&deb_ev, 0, sizeof(deb_ev)); while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); @@ -119,59 +121,28 @@ void DebugDebuggee(PROCESS_INFORMATION& pi) { case LOAD_DLL_DEBUG_EVENT: case RIP_EVENT: case UNLOAD_DLL_DEBUG_EVENT: + // Continue on all other debug events ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); break; - //~ case EXIT_THREAD_DEBUG_EVENT: { - //~ const DWORD& exit_code = deb_ev.u.ExitThread.dwExitCode; - - //~ // Generate a crash dump on the first abnormal thread exit. - //~ // We don't want to generate on every abnormal thread exit since ALL the other - // threads ~ // in the application will follow by exiting with the same code. ~ if - //(!seen_nonzero_thread_exit && exit_code != 0) { ~ seen_nonzero_thread_exit = true; ~ - // std::fprintf(stderr, ~ "Creating MiniDump on first non-zero thread exit: code - // 0x%08x\n", ~ exit_code); - //~ DumpFromDebugEvent(deb_ev, pi); - //~ } - //~ ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); - //~ break; - //~ } - case EXCEPTION_DEBUG_EVENT: { + case EXCEPTION_DEBUG_EVENT: EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; - const std::time_t now = std::time(nullptr); - const std::time_t delta = now - start_time; - if (ExceptionName(record.ExceptionCode) == nullptr) { - int record_count = 0; - EXCEPTION_RECORD* next_record = &deb_ev.u.Exception.ExceptionRecord; - while (next_record != nullptr) { - std::fprintf(stderr, - "[%d] code(%d): 0x%08x\n\tflags: %08x %s\n\taddress: " - "0x%08x\n\tparameters: %d\n", - delta, record_count, next_record->ExceptionCode, - next_record->ExceptionFlags, - next_record->ExceptionFlags == EXCEPTION_NONCONTINUABLE - ? "noncontinuable" - : "", - next_record->ExceptionAddress, next_record->NumberParameters); - for (int i = 0; i < static_cast(next_record->NumberParameters); i++) { - std::fprintf(stderr, "\t\t%0d: 0x%08x\n", i, - next_record->ExceptionInformation[i]); - } - - record_count++; - next_record = next_record->ExceptionRecord; - } - } // We want to generate a crash dump if we are seeing the same exception again. if (!deb_ev.u.Exception.dwFirstChance) { std::fprintf(stderr, "Creating MiniDump on ExceptionCode: 0x%08x %s\n", record.ExceptionCode, ExceptionName(record.ExceptionCode)); DumpFromDebugEvent(deb_ev, pi); } + + // Continue without handling the exception. + // Lets the debuggee use its own exception handler. + // - If one does not exist, we will see the exception once more where we make a minidump + // for. Then when it reaches here again, yuzu will probably crash. + // - DBG_CONTINUE on an exception that the debuggee does not handle can set us up for an + // infinite loop of exceptions. ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); break; } - } } } diff --git a/src/yuzu/mini_dump.h b/src/yuzu/mini_dump.h index f6f5dc2c7..2052e5248 100644 --- a/src/yuzu/mini_dump.h +++ b/src/yuzu/mini_dump.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #pragma once #include From c976613ab25130e8fc8b6c0de03cf380eb46c64e Mon Sep 17 00:00:00 2001 From: lat9nq Date: Mon, 25 Jul 2022 16:45:38 -0400 Subject: [PATCH 4/7] vcpkg,cmake: Use vcpkg for dbghelp --- CMakeLists.txt | 12 ++++++++++++ src/yuzu/CMakeLists.txt | 2 +- vcpkg.json | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ab0ea589..20dd1383f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,8 @@ option(YUZU_USE_BUNDLED_OPUS "Compile bundled opus" ON) option(YUZU_TESTS "Compile tests" ON) +CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile Windows crash dump (Minidump) support" OFF "WIN32" OFF) + option(YUZU_USE_BUNDLED_VCPKG "Use vcpkg for yuzu dependencies" "${MSVC}") option(YUZU_CHECK_SUBMODULES "Check if submodules are present" ON) @@ -46,6 +48,9 @@ if (YUZU_USE_BUNDLED_VCPKG) if (YUZU_TESTS) list(APPEND VCPKG_MANIFEST_FEATURES "yuzu-tests") endif() + if (YUZU_CRASH_DUMPS) + list(APPEND VCPKG_MANIFEST_FEATURES "dbghelp") + endif() include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake) elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "") @@ -447,6 +452,13 @@ elseif (WIN32) # PSAPI is the Process Status API set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} psapi imm32 version) endif() + + if (YUZU_CRASH_DUMPS) + find_library(DBGHELP_LIBRARY dbghelp) + if ("${DBGHELP_LIBRARY}" STREQUAL "DBGHELP_LIBRARY-NOTFOUND") + message(FATAL_ERROR "YUZU_CRASH_DUMPS enabled but dbghelp library not found") + endif() + endif() elseif (CMAKE_SYSTEM_NAME MATCHES "^(Linux|kFreeBSD|GNU|SunOS)$") set(PLATFORM_LIBRARIES rt) endif() diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 3d9906ade..df0f64b83 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -208,7 +208,7 @@ add_executable(yuzu yuzu.rc ) -if (WIN32 AND NOT ("${DBGHELP_LIBRARY}" STREQUAL "DBGHELP_LIBRARY-NOTFOUND")) +if (WIN32 AND YUZU_CRASH_DUMPS) target_sources(yuzu PRIVATE mini_dump.cpp mini_dump.h diff --git a/vcpkg.json b/vcpkg.json index c4413e22a..3c92510d6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -31,6 +31,10 @@ "yuzu-tests": { "description": "Compile tests", "dependencies": [ "catch2" ] + }, + "dbghelp": { + "description": "Compile Windows crash dump (Minidump) support", + "dependencies": [ "dbghelp" ] } }, "overrides": [ From 45b343d1d09f30193a1da52924ee83a834356fae Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sat, 30 Jul 2022 09:18:33 -0400 Subject: [PATCH 5/7] ci,workflows: Enable crash dumps on MSVC builds ci/windows: Enable crash dumps on MinGW builds --- .ci/scripts/windows/docker.sh | 1 + .ci/templates/build-msvc.yml | 2 +- .github/workflows/verify.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.ci/scripts/windows/docker.sh b/.ci/scripts/windows/docker.sh index 790ba8218..6f522feed 100755 --- a/.ci/scripts/windows/docker.sh +++ b/.ci/scripts/windows/docker.sh @@ -21,6 +21,7 @@ cmake .. \ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ -DENABLE_QT_TRANSLATION=ON \ -DUSE_CCACHE=ON \ + -DYUZU_CRASH_DUMPS=ON \ -DYUZU_USE_BUNDLED_SDL2=OFF \ -DYUZU_USE_EXTERNAL_SDL2=OFF \ -DYUZU_TESTS=OFF \ diff --git a/.ci/templates/build-msvc.yml b/.ci/templates/build-msvc.yml index a2ee71bd8..ea405e5dc 100644 --- a/.ci/templates/build-msvc.yml +++ b/.ci/templates/build-msvc.yml @@ -9,7 +9,7 @@ parameters: steps: - script: choco install vulkan-sdk displayName: 'Install vulkan-sdk' -- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release .. && cd .. +- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release -DYUZU_CRASH_DUMPS=ON .. && cd .. displayName: 'Configure CMake' - task: MSBuild@1 displayName: 'Build' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 733a52764..7cde8380b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -104,7 +104,7 @@ jobs: run: | glslangValidator --version mkdir build - cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DGIT_BRANCH=pr-verify + cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DGIT_BRANCH=pr-verify -DYUZU_CRASH_DUMPS=ON - name: Build run: cmake --build build - name: Cache Summary From 12f7d42d32511955ee27875d42b6e8e3cda9e523 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sat, 30 Jul 2022 10:23:14 -0400 Subject: [PATCH 6/7] mini_dump: Address review feedback Uses fmt::print as opposed to std::fprintf. Adds a missing return. static's a single-use function. Initializes structs as opposed to std::memset where possible. Fixes CMake linkage. Co-authored-by: Lioncash mini_dump: Use a namespace Co-authored-by: Lioncash --- src/yuzu/CMakeLists.txt | 2 +- src/yuzu/main.cpp | 5 +- src/yuzu/mini_dump.cpp | 122 +++++++++++++++++++++------------------- src/yuzu/mini_dump.h | 5 +- 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index df0f64b83..29d506c47 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -214,7 +214,7 @@ if (WIN32 AND YUZU_CRASH_DUMPS) mini_dump.h ) - target_link_libraries(yuzu PUBLIC ${DBGHELP_LIBRARY}) + target_link_libraries(yuzu PRIVATE ${DBGHELP_LIBRARY}) target_compile_definitions(yuzu PRIVATE -DYUZU_DBGHELP) endif() diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ff59c64c3..bda9986e1 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -4096,10 +4096,11 @@ int main(int argc, char* argv[]) { #ifdef YUZU_DBGHELP PROCESS_INFORMATION pi; - if (!is_child && Settings::values.create_crash_dumps.GetValue() && SpawnDebuggee(argv[0], pi)) { + if (!is_child && Settings::values.create_crash_dumps.GetValue() && + MiniDump::SpawnDebuggee(argv[0], pi)) { // Delete the config object so that it doesn't save when the program exits config.reset(nullptr); - DebugDebuggee(pi); + MiniDump::DebugDebuggee(pi); return 0; } #endif diff --git a/src/yuzu/mini_dump.cpp b/src/yuzu/mini_dump.cpp index b25067c10..a34dc6a9c 100644 --- a/src/yuzu/mini_dump.cpp +++ b/src/yuzu/mini_dump.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "yuzu/mini_dump.h" #include "yuzu/startup_checks.h" @@ -12,6 +13,8 @@ // dbghelp.h must be included after windows.h #include +namespace MiniDump { + void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, EXCEPTION_POINTERS* pep) { char file_name[255]; @@ -23,7 +26,7 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_ CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file_handle == nullptr || file_handle == INVALID_HANDLE_VALUE) { - std::fprintf(stderr, "CreateFileA failed. Error: %d\n", GetLastError()); + fmt::print(stderr, "CreateFileA failed. Error: {}", GetLastError()); return; } @@ -34,9 +37,9 @@ void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_ dump_type, (pep != 0) ? info : 0, 0, 0); if (write_dump_status) { - std::fprintf(stderr, "MiniDump created: %s\n", file_name); + fmt::print(stderr, "MiniDump created: {}", file_name); } else { - std::fprintf(stderr, "MiniDumpWriteDump failed. Error: %d\n", GetLastError()); + fmt::print(stderr, "MiniDumpWriteDump failed. Error: {}", GetLastError()); } // Close the file @@ -48,15 +51,15 @@ void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { HANDLE thread_handle = OpenThread(THREAD_GET_CONTEXT, false, deb_ev.dwThreadId); if (thread_handle == nullptr) { - std::fprintf(stderr, "OpenThread failed (%d)\n", GetLastError()); + fmt::print(stderr, "OpenThread failed ({})", GetLastError()); + return; } // Get child process context - CONTEXT context; - std::memset(&context, 0, sizeof(context)); + CONTEXT context = {}; context.ContextFlags = CONTEXT_ALL; if (!GetThreadContext(thread_handle, &context)) { - std::fprintf(stderr, "GetThreadContext failed (%d)\n", GetLastError()); + fmt::print(stderr, "GetThreadContext failed ({})", GetLastError()); return; } @@ -73,7 +76,7 @@ void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep); if (CloseHandle(thread_handle) == 0) { - std::fprintf(stderr, "error: CloseHandle(thread_handle) failed (%d)\n", GetLastError()); + fmt::print(stderr, "error: CloseHandle(thread_handle) failed ({})", GetLastError()); } } @@ -86,67 +89,22 @@ bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { } if (!SpawnChild(arg0, &pi, 0)) { - std::fprintf(stderr, "warning: continuing without crash dumps\n"); + fmt::print(stderr, "warning: continuing without crash dumps"); return false; } const bool can_debug = DebugActiveProcess(pi.dwProcessId); if (!can_debug) { - std::fprintf(stderr, - "warning: DebugActiveProcess failed (%d), continuing without crash dumps\n", - GetLastError()); + fmt::print(stderr, + "warning: DebugActiveProcess failed ({}), continuing without crash dumps", + GetLastError()); return false; } return true; } -void DebugDebuggee(PROCESS_INFORMATION& pi) { - DEBUG_EVENT deb_ev; - std::memset(&deb_ev, 0, sizeof(deb_ev)); - - while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { - const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); - if (!wait_success) { - std::fprintf(stderr, "error: WaitForDebugEvent failed (%d)\n", GetLastError()); - return; - } - - switch (deb_ev.dwDebugEventCode) { - case OUTPUT_DEBUG_STRING_EVENT: - case CREATE_PROCESS_DEBUG_EVENT: - case CREATE_THREAD_DEBUG_EVENT: - case EXIT_PROCESS_DEBUG_EVENT: - case EXIT_THREAD_DEBUG_EVENT: - case LOAD_DLL_DEBUG_EVENT: - case RIP_EVENT: - case UNLOAD_DLL_DEBUG_EVENT: - // Continue on all other debug events - ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); - break; - case EXCEPTION_DEBUG_EVENT: - EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; - - // We want to generate a crash dump if we are seeing the same exception again. - if (!deb_ev.u.Exception.dwFirstChance) { - std::fprintf(stderr, "Creating MiniDump on ExceptionCode: 0x%08x %s\n", - record.ExceptionCode, ExceptionName(record.ExceptionCode)); - DumpFromDebugEvent(deb_ev, pi); - } - - // Continue without handling the exception. - // Lets the debuggee use its own exception handler. - // - If one does not exist, we will see the exception once more where we make a minidump - // for. Then when it reaches here again, yuzu will probably crash. - // - DBG_CONTINUE on an exception that the debuggee does not handle can set us up for an - // infinite loop of exceptions. - ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); - break; - } - } -} - -const char* ExceptionName(DWORD exception) { +static const char* ExceptionName(DWORD exception) { switch (exception) { case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION"; @@ -193,6 +151,52 @@ const char* ExceptionName(DWORD exception) { case EXCEPTION_INVALID_HANDLE: return "EXCEPTION_INVALID_HANDLE"; default: - return nullptr; + return "unknown exception type"; } } + +void DebugDebuggee(PROCESS_INFORMATION& pi) { + DEBUG_EVENT deb_ev = {}; + + while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { + const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); + if (!wait_success) { + fmt::print(stderr, "error: WaitForDebugEvent failed ({})", GetLastError()); + return; + } + + switch (deb_ev.dwDebugEventCode) { + case OUTPUT_DEBUG_STRING_EVENT: + case CREATE_PROCESS_DEBUG_EVENT: + case CREATE_THREAD_DEBUG_EVENT: + case EXIT_PROCESS_DEBUG_EVENT: + case EXIT_THREAD_DEBUG_EVENT: + case LOAD_DLL_DEBUG_EVENT: + case RIP_EVENT: + case UNLOAD_DLL_DEBUG_EVENT: + // Continue on all other debug events + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); + break; + case EXCEPTION_DEBUG_EVENT: + EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + + // We want to generate a crash dump if we are seeing the same exception again. + if (!deb_ev.u.Exception.dwFirstChance) { + fmt::print(stderr, "Creating MiniDump on ExceptionCode: 0x{:08x} {}\n", + record.ExceptionCode, ExceptionName(record.ExceptionCode)); + DumpFromDebugEvent(deb_ev, pi); + } + + // Continue without handling the exception. + // Lets the debuggee use its own exception handler. + // - If one does not exist, we will see the exception once more where we make a minidump + // for. Then when it reaches here again, yuzu will probably crash. + // - DBG_CONTINUE on an exception that the debuggee does not handle can set us up for an + // infinite loop of exceptions. + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); + break; + } + } +} + +} // namespace MiniDump diff --git a/src/yuzu/mini_dump.h b/src/yuzu/mini_dump.h index 2052e5248..d6b6cca84 100644 --- a/src/yuzu/mini_dump.h +++ b/src/yuzu/mini_dump.h @@ -7,10 +7,13 @@ #include +namespace MiniDump { + void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, EXCEPTION_POINTERS* pep); void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi); bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi); void DebugDebuggee(PROCESS_INFORMATION& pi); -const char* ExceptionName(DWORD exception); + +} // namespace MiniDump From 9dc9e501debda06ff31a3e2fe8875ffa0d94e40b Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sat, 30 Jul 2022 12:41:30 -0400 Subject: [PATCH 7/7] ci/windows: Upload debugging symbols --- .ci/scripts/windows/upload.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/scripts/windows/upload.ps1 b/.ci/scripts/windows/upload.ps1 index d463281de..21abcd752 100644 --- a/.ci/scripts/windows/upload.ps1 +++ b/.ci/scripts/windows/upload.ps1 @@ -65,8 +65,8 @@ if ("$env:GITHUB_ACTIONS" -eq "true") { # None of the other GHA builds are including source, so commenting out today #Copy-Item $MSVC_SOURCE_TARXZ -Destination "artifacts" - # Are debug symbols important? - # cp .\build\bin\yuzu*.pdb .\pdb\ + # Debugging symbols + cp .\build\bin\yuzu*.pdb .\artifacts\ # Write out a tag BUILD_TAG to environment for the Upload step # We're getting ${{ github.event.number }} as $env:PR_NUMBER"