mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
Most crashes are caught by the game's own fatal handlers, which write a dump of thread stacks only; the launcher could only copy it after the game had exited. A crash on a freed or overwritten object then showed the faulting registers but not the object or the list it came from. - game_fatal_dump: the game's MiniDumpWriteDump import points at ReSkate. Before the game writes its dump, the crash reporter takes its own of the still-running game for the faulting thread; the game's dump follows as before. Reports say capture.source "game-fatal-handler". - The reporter's dumps include the memory the stacks point at (MiniDumpWithIndirectlyReferencedMemory), a few MB more a report. - The shared report says who asked for the dump (protocol version 4). - Tests: a game-handler crash is captured with a heap block only its stack points at; ReSkate's own capture keeps it too.
150 lines
8.4 KiB
C++
150 lines
8.4 KiB
C++
#include <Windows.h>
|
|
#include "crash_reporter.h"
|
|
#include <DbgHelp.h>
|
|
#include "Engine/Core/Debug/protocol.h"
|
|
#include "Engine/Core/Debug/upload.h"
|
|
#include "Engine/Core/Debug/native_dump.h"
|
|
#include "Engine/Core/Debug/multipart.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "crash_report_config.h"
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <format>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
using namespace dingosdk;
|
|
using namespace dingosdk::crash_report;
|
|
std::string utf8(const wchar_t* text) {
|
|
const auto size = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
|
|
if (size <= 1) return {};
|
|
std::string result(static_cast<std::size_t>(size), '\0');
|
|
WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size, nullptr, nullptr);
|
|
result.pop_back();
|
|
return result;
|
|
}
|
|
template<std::size_t N> std::string field(const std::array<wchar_t, N>& value) {
|
|
return utf8(std::wstring(value.data(), wcsnlen(value.data(), N)).c_str());
|
|
}
|
|
// "Levels/Game/DingoLevel_Root/DingoLevel_Root + levels/game/x/x @ Start" -> "x": the sublevel when
|
|
// there is one, else the level, by its last name.
|
|
std::string map_name(std::string destination) {
|
|
if (const auto at = destination.find(" @ "); at != std::string::npos) destination.resize(at);
|
|
if (const auto plus = destination.find(" + "); plus != std::string::npos) destination.erase(0, plus + 3);
|
|
if (const auto slash = destination.find_last_of('/'); slash != std::string::npos) destination.erase(0, slash + 1);
|
|
return destination;
|
|
}
|
|
bool publish_dump(const std::filesystem::path& temporary, const SharedReport& report, const char* source) {
|
|
auto dump = temporary; dump.replace_extension(L".dmp");
|
|
Json attributes = Json::object();
|
|
attributes["application"] = "ReSkate";
|
|
attributes["process"] = utf8(report.application.data());
|
|
attributes["version"] = build_version;
|
|
#ifdef _DEBUG
|
|
attributes["build.configuration"] = "Debug";
|
|
#else
|
|
attributes["build.configuration"] = "Release";
|
|
#endif
|
|
attributes["game.supported_sha256"] = supported_build::game_sha256;
|
|
attributes["error.type"] = "crash";
|
|
attributes["report.id"] = utf8(dump.stem().c_str());
|
|
attributes["capture.source"] = source;
|
|
// (Tags for grouping crashes by the level and the graphics card; absent when unknown.)
|
|
if (const auto destination = field(report.destination); !destination.empty()) {
|
|
attributes["game.destination"] = destination;
|
|
attributes["game.map"] = map_name(destination);
|
|
}
|
|
if (const auto gpu = field(report.gpu_name); !gpu.empty()) attributes["gpu.name"] = gpu;
|
|
if (const auto driver = field(report.gpu_driver); !driver.empty()) attributes["gpu.driver"] = driver;
|
|
const auto log = snapshot_log(std::filesystem::path(report.directory.data()).parent_path() / L"ReSkate.log", dump);
|
|
attributes["attachment.log"] = log == LogSnapshot::complete ? "complete" : log == LogSnapshot::tail ? "tail" : "unavailable";
|
|
std::ofstream metadata(dump.wstring() + L".json", std::ios::binary);
|
|
metadata << attributes.dump();
|
|
metadata.close();
|
|
if (!metadata || !MoveFileExW(temporary.c_str(), dump.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
|
DeleteFileW(temporary.c_str());
|
|
DeleteFileW((dump.wstring() + L".json").c_str());
|
|
DeleteFileW(log_attachment_path(dump).c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool save_dump(HANDLE process, const SharedReport& report, const std::filesystem::path& directory) {
|
|
const auto id = std::format(L"{}-{}-{}", GetCurrentProcessId(), GetTickCount64(), report.thread_id);
|
|
const auto temporary = directory / (id + L".tmp");
|
|
Handle file(CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
|
|
if (file.value == INVALID_HANDLE_VALUE) return false;
|
|
MINIDUMP_EXCEPTION_INFORMATION exception{report.thread_id, report.exception, TRUE};
|
|
// With the memory the stacks point at, so a crash on a freed or overwritten object shows that
|
|
// object and the list it was found in, not just the registers (a few MB more a dump).
|
|
constexpr auto flags = static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithThreadInfo |
|
|
MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory);
|
|
const auto written = MiniDumpWriteDump(process, GetProcessId(process), file.value, flags, &exception, nullptr, nullptr);
|
|
const auto durable = written && FlushFileBuffers(file.value);
|
|
CloseHandle(file.release());
|
|
if (!durable) { DeleteFileW(temporary.c_str()); return false; }
|
|
return publish_dump(temporary, report,
|
|
report.source == CaptureSource::game_fatal_handler ? "game-fatal-handler" : "unhandled-exception");
|
|
}
|
|
bool collect_native_dump(HANDLE process, const SharedReport& report, const std::filesystem::path& directory) {
|
|
const auto native = find_native_dump(report.native_directory.data(), GetProcessId(process), process_creation_seconds(process));
|
|
if (!native) return false;
|
|
const auto temporary = directory / std::format(L"native-{}-{}-{}.tmp", native->process_id, native->process_created, native->thread_id);
|
|
if (!CopyFileW(native->path.c_str(), temporary.c_str(), TRUE)) return false;
|
|
return publish_dump(temporary, report, "game-native");
|
|
}
|
|
int run(int argc, wchar_t** argv) {
|
|
if (argc != handle_count + 2 || std::wstring_view(argv[1]) != L"--reskate-crash-helper") return 2;
|
|
std::array<Handle, handle_count> handles;
|
|
for (std::size_t i = 0; i < handle_count; ++i) {
|
|
const std::wstring_view value(argv[i + 2]);
|
|
if (value.empty() || value.find_first_not_of(L"0123456789") != std::wstring::npos) return 2;
|
|
wchar_t* end{};
|
|
const auto number = wcstoull(argv[i + 2], &end, 10);
|
|
if (!number || *end) return 2;
|
|
handles[i].value = reinterpret_cast<HANDLE>(number);
|
|
DWORD flags{};
|
|
if (!GetHandleInformation(handles[i].value, &flags)) return 2;
|
|
SetHandleInformation(handles[i].value, HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
auto* shared = static_cast<SharedReport*>(MapViewOfFile(handles[mapping].value, FILE_MAP_READ, 0, 0, sizeof(SharedReport)));
|
|
if (!shared) return 3;
|
|
struct View { void* value; ~View() { UnmapViewOfFile(value); } } view{shared};
|
|
if (shared->version != protocol_version || shared->url.back() || shared->directory.back() ||
|
|
shared->application.back() || shared->native_directory.back()) return 3;
|
|
const std::wstring url(shared->url.data());
|
|
Endpoint endpoint;
|
|
if (!parse_endpoint(url, endpoint)) return 3;
|
|
const auto directory = queue_directory(shared->directory.data(), url);
|
|
std::filesystem::create_directories(directory);
|
|
// Uploading an old queue must never delay capturing a new crash.
|
|
std::jthread uploader([&] { upload_pending(url, directory); });
|
|
SetEvent(handles[ready].value);
|
|
const HANDLE wait_for[]{handles[requested].value, handles[stopping].value, handles[parent].value};
|
|
const auto reason = WaitForMultipleObjects(3, wait_for, FALSE, INFINITE);
|
|
bool saved{};
|
|
if (reason == WAIT_OBJECT_0) {
|
|
try { saved = save_dump(handles[parent].value, *shared, directory); } catch (...) {}
|
|
// Release the crashed process before any network activity or uploader join.
|
|
SetEvent(handles[captured].value);
|
|
std::ofstream(directory / L"capture-status.txt") << (saved ? "Captured unhandled exception.\n" : "Could not capture minidump; local crash log retained.\n");
|
|
} else if (reason == WAIT_OBJECT_0 + 2 && shared->native_directory[0]) {
|
|
// The game's native fatal handler may replace/bypass our Windows filter.
|
|
// After exit all native dump writers are closed. Preserve their original
|
|
// exception context rather than taking a context-free termination dump.
|
|
try { saved = collect_native_dump(handles[parent].value, *shared, directory); } catch (...) {}
|
|
DWORD exit_code{}; GetExitCodeProcess(handles[parent].value, &exit_code);
|
|
std::ofstream(directory / L"capture-status.txt") << std::format(
|
|
"Game process {} exited (0x{:08x}); {}\n", GetProcessId(handles[parent].value), exit_code,
|
|
saved ? "queued its native crash dump." : "no native dump matched this process and start time.");
|
|
} else return 0;
|
|
uploader.join();
|
|
if (saved) upload_pending(url, directory);
|
|
return saved ? 0 : 4;
|
|
}
|
|
}
|
|
int dingosdk::crash_report::run_reporter(int argc, wchar_t** argv) noexcept {
|
|
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
|
|
try { return run(argc, argv); } catch (...) { return 1; }
|
|
}
|