21 changed files with 223 additions and 282 deletions
-
1.ci/scripts/clang/docker.sh
-
1.ci/scripts/linux/docker.sh
-
1.ci/scripts/windows/docker.sh
-
3.gitmodules
-
24CMakeLists.txt
-
102externals/CMakeLists.txt
-
1externals/breakpad
-
2externals/dynarmic
-
1src/common/fs/fs_paths.h
-
1src/common/fs/path_util.cpp
-
1src/common/fs/path_util.h
-
1src/common/settings.h
-
10src/yuzu/CMakeLists.txt
-
77src/yuzu/breakpad.cpp
-
10src/yuzu/breakpad.h
-
18src/yuzu/configuration/configure_debug.cpp
-
7src/yuzu/configuration/configure_debug.ui
-
19src/yuzu/main.cpp
-
202src/yuzu/mini_dump.cpp
-
19src/yuzu/mini_dump.h
-
4vcpkg.json
@ -1 +1 @@ |
|||
Subproject commit 7da378033a7764f955516f75194856d87bbcd7a5 |
|||
Subproject commit 0df09e2f6b61c2d7ad2f2053d4f020a5c33e0378 |
|||
@ -0,0 +1,77 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <algorithm>
|
|||
#include <ranges>
|
|||
|
|||
#if defined(_WIN32)
|
|||
#include <client/windows/handler/exception_handler.h>
|
|||
#elif defined(__linux__)
|
|||
#include <client/linux/handler/exception_handler.h>
|
|||
#else
|
|||
#error Minidump creation not supported on this platform
|
|||
#endif
|
|||
|
|||
#include "common/fs/fs_paths.h"
|
|||
#include "common/fs/path_util.h"
|
|||
#include "yuzu/breakpad.h"
|
|||
|
|||
namespace Breakpad { |
|||
|
|||
static void PruneDumpDirectory(const std::filesystem::path& dump_path) { |
|||
// Code in this function should be exception-safe.
|
|||
struct Entry { |
|||
std::filesystem::path path; |
|||
std::filesystem::file_time_type last_write_time; |
|||
}; |
|||
std::vector<Entry> existing_dumps; |
|||
|
|||
// Get existing entries.
|
|||
std::error_code ec; |
|||
std::filesystem::directory_iterator dir(dump_path, ec); |
|||
for (auto& entry : dir) { |
|||
if (entry.is_regular_file()) { |
|||
existing_dumps.push_back(Entry{ |
|||
.path = entry.path(), |
|||
.last_write_time = entry.last_write_time(ec), |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// Sort descending by creation date.
|
|||
std::ranges::stable_sort(existing_dumps, [](const auto& a, const auto& b) { |
|||
return a.last_write_time > b.last_write_time; |
|||
}); |
|||
|
|||
// Delete older dumps.
|
|||
for (size_t i = 5; i < existing_dumps.size(); i++) { |
|||
std::filesystem::remove(existing_dumps[i].path, ec); |
|||
} |
|||
} |
|||
|
|||
#if defined(__linux__)
|
|||
[[noreturn]] bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, |
|||
bool succeeded) { |
|||
// Prevent time- and space-consuming core dumps from being generated, as we have
|
|||
// already generated a minidump and a core file will not be useful anyway.
|
|||
_exit(1); |
|||
} |
|||
#endif
|
|||
|
|||
void InstallCrashHandler() { |
|||
// Write crash dumps to profile directory.
|
|||
const auto dump_path = GetYuzuPath(Common::FS::YuzuPath::CrashDumpsDir); |
|||
PruneDumpDirectory(dump_path); |
|||
|
|||
#if defined(_WIN32)
|
|||
// TODO: If we switch to MinGW builds for Windows, this needs to be wrapped in a C API.
|
|||
static google_breakpad::ExceptionHandler eh{dump_path, nullptr, nullptr, nullptr, |
|||
google_breakpad::ExceptionHandler::HANDLER_ALL}; |
|||
#elif defined(__linux__)
|
|||
static google_breakpad::MinidumpDescriptor descriptor{dump_path}; |
|||
static google_breakpad::ExceptionHandler eh{descriptor, nullptr, DumpCallback, |
|||
nullptr, true, -1}; |
|||
#endif
|
|||
} |
|||
|
|||
} // namespace Breakpad
|
|||
@ -0,0 +1,10 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
namespace Breakpad { |
|||
|
|||
void InstallCrashHandler(); |
|||
|
|||
} |
|||
@ -1,202 +0,0 @@ |
|||
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <cstdio>
|
|||
#include <cstring>
|
|||
#include <ctime>
|
|||
#include <filesystem>
|
|||
#include <fmt/format.h>
|
|||
#include <windows.h>
|
|||
#include "yuzu/mini_dump.h"
|
|||
#include "yuzu/startup_checks.h"
|
|||
|
|||
// dbghelp.h must be included after windows.h
|
|||
#include <dbghelp.h>
|
|||
|
|||
namespace MiniDump { |
|||
|
|||
void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, |
|||
EXCEPTION_POINTERS* pep) { |
|||
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 = CreateFileA(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr, |
|||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); |
|||
|
|||
if (file_handle == nullptr || file_handle == INVALID_HANDLE_VALUE) { |
|||
fmt::print(stderr, "CreateFileA failed. Error: {}", 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) { |
|||
fmt::print(stderr, "MiniDump created: {}", file_name); |
|||
} else { |
|||
fmt::print(stderr, "MiniDumpWriteDump failed. Error: {}", GetLastError()); |
|||
} |
|||
|
|||
// Close the file
|
|||
CloseHandle(file_handle); |
|||
} |
|||
|
|||
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) { |
|||
fmt::print(stderr, "OpenThread failed ({})", GetLastError()); |
|||
return; |
|||
} |
|||
|
|||
// Get child process context
|
|||
CONTEXT context = {}; |
|||
context.ContextFlags = CONTEXT_ALL; |
|||
if (!GetThreadContext(thread_handle, &context)) { |
|||
fmt::print(stderr, "GetThreadContext failed ({})", 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) { |
|||
fmt::print(stderr, "error: CloseHandle(thread_handle) failed ({})", GetLastError()); |
|||
} |
|||
} |
|||
|
|||
bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { |
|||
std::memset(&pi, 0, sizeof(pi)); |
|||
|
|||
// Don't debug if we are already being debugged
|
|||
if (IsDebuggerPresent()) { |
|||
return false; |
|||
} |
|||
|
|||
if (!SpawnChild(arg0, &pi, 0)) { |
|||
fmt::print(stderr, "warning: continuing without crash dumps"); |
|||
return false; |
|||
} |
|||
|
|||
const bool can_debug = DebugActiveProcess(pi.dwProcessId); |
|||
if (!can_debug) { |
|||
fmt::print(stderr, |
|||
"warning: DebugActiveProcess failed ({}), continuing without crash dumps", |
|||
GetLastError()); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static 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 "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
|
|||
@ -1,19 +0,0 @@ |
|||
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <windows.h> |
|||
|
|||
#include <dbghelp.h> |
|||
|
|||
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); |
|||
|
|||
} // namespace MiniDump |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue