mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 21:25:21 +02:00
Crash dumps now go to the project's own Sentry server. The launcher's out-of-process helper is unchanged; only where it posts and what it expects back are: - The default endpoint is the Sentry project's minidump URL. Report attributes travel as Sentry tags, with the release as reskate@<version>. - An upload counts as accepted when the server answers with an event ID. - "backtrace" is "crash_report" throughout: the namespace, files, CMake targets, DINGOSDK_CRASH_REPORT_URL, DINGOSDK_BUILD_CRASH_REPORT_TESTS and the RESKATE_CRASH_REPORT_URL override. Performance timings are new, and off unless "Send performance timings" is ticked in the launcher's settings (RESKATE_TRACING=1). A small tracer (Engine/Core/Debug/trace.h) posts each finished step to Sentry as a transaction from a background thread; the launcher and the game it starts share one trace. Timed: the update check, the Steam download, Thunderstore installs, the mod merge, starting the game, the launch up to the first playable level, each level load, joining a session, fetching the identity lists, and the time spent in each level with its average frame rate and long frames. Turning crash reports off turns timings off too.
124 lines
6.8 KiB
C++
124 lines
6.8 KiB
C++
#include "crash_report.h"
|
|
#include "protocol.h"
|
|
#include "crash_report_config.h"
|
|
#include <algorithm>
|
|
#include <format>
|
|
#include <vector>
|
|
|
|
namespace dingosdk::crash_report {
|
|
namespace {
|
|
std::array<HANDLE, handle_count> handles{};
|
|
HANDLE helper{};
|
|
SharedReport* report{};
|
|
// 0 = ready, 1 = capture owns the IPC, 2 = stopped/initializing.
|
|
volatile LONG capturing{2};
|
|
struct View {
|
|
void* value{};
|
|
~View() { if (value) UnmapViewOfFile(value); }
|
|
};
|
|
template<std::size_t N> bool copy(std::array<wchar_t, N>& target, const std::wstring& value) {
|
|
if (value.size() >= N) return false;
|
|
std::copy(value.begin(), value.end(), target.begin());
|
|
target[value.size()] = 0;
|
|
return true;
|
|
}
|
|
}
|
|
StartResult start(const std::filesystem::path& log_directory) noexcept {
|
|
try {
|
|
if (report) return StartResult::ready;
|
|
if (environment(L"RESKATE_CRASH_REPORTING") == L"0") return StartResult::disabled;
|
|
auto url = environment(L"RESKATE_CRASH_REPORT_URL");
|
|
if (url.empty()) url = configured_url;
|
|
if (url.empty()) return StartResult::disabled;
|
|
if (!url.starts_with(L"https://") || url.size() >= SharedReport{}.url.size()) return StartResult::failed;
|
|
HMODULE module{};
|
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
reinterpret_cast<LPCWSTR>(&handles), &module)) return StartResult::failed;
|
|
std::array<wchar_t, 32768> module_path{}, process_path{};
|
|
const auto module_size = GetModuleFileNameW(module, module_path.data(), static_cast<DWORD>(module_path.size()));
|
|
const auto process_size = GetModuleFileNameW(nullptr, process_path.data(), static_cast<DWORD>(process_path.size()));
|
|
if (!module_size || module_size >= module_path.size() || !process_size || process_size >= process_path.size())
|
|
return StartResult::failed;
|
|
const auto executable = std::filesystem::path(module_path.data()).parent_path() / L"ReSkateLauncher.exe";
|
|
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
|
|
std::array<Handle, handle_count> owned;
|
|
owned[mapping].value = CreateFileMappingW(INVALID_HANDLE_VALUE, &security, PAGE_READWRITE, 0, sizeof(SharedReport), nullptr);
|
|
for (std::size_t i = requested; i <= ready; ++i) owned[i].value = CreateEventW(&security, TRUE, FALSE, nullptr);
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &owned[parent].value,
|
|
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, 0)) return StartResult::failed;
|
|
std::array<HANDLE, handle_count> inherited{};
|
|
for (std::size_t i = 0; i < handle_count; ++i) {
|
|
if (!owned[i].value) return StartResult::failed;
|
|
inherited[i] = owned[i].value;
|
|
}
|
|
View view{MapViewOfFile(owned[mapping].value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedReport))};
|
|
if (!view.value) return StartResult::failed;
|
|
auto* shared = new (view.value) SharedReport{};
|
|
if (!copy(shared->url, url) || !copy(shared->directory, std::filesystem::absolute(log_directory / L"crashes").wstring()) ||
|
|
!copy(shared->application, std::filesystem::path(process_path.data()).filename().wstring())) return StartResult::failed;
|
|
// Frostbite handles some fatal exceptions itself and writes a native dump
|
|
// before exiting, so those faults never reach our last-chance filter. The
|
|
// game's LocalAppData is redirected to ReSkate\Game (Extension/Boot/user_data_redirect.cpp).
|
|
if (_wcsicmp(shared->application.data(), L"Skate.exe") == 0) {
|
|
const auto local = environment(L"LOCALAPPDATA");
|
|
if (!local.empty() && !copy(shared->native_directory,
|
|
(std::filesystem::path(local) / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps").wstring()))
|
|
return StartResult::failed;
|
|
}
|
|
|
|
SIZE_T size{};
|
|
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
|
std::vector<unsigned char> storage(size);
|
|
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
|
|
if (!InitializeProcThreadAttributeList(attributes, 1, 0, &size)) return StartResult::failed;
|
|
struct Attributes { LPPROC_THREAD_ATTRIBUTE_LIST value; ~Attributes() { DeleteProcThreadAttributeList(value); } } cleanup{attributes};
|
|
if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
|
inherited.data(), sizeof(inherited), nullptr, nullptr)) return StartResult::failed;
|
|
auto command = std::format(L"\"{}\" --reskate-crash-helper", executable.wstring());
|
|
for (const auto handle : inherited) command += std::format(L" {}", reinterpret_cast<ULONG_PTR>(handle));
|
|
STARTUPINFOEXW startup{};
|
|
startup.StartupInfo.cb = sizeof(startup);
|
|
startup.lpAttributeList = attributes;
|
|
PROCESS_INFORMATION process{};
|
|
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
|
|
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, executable.parent_path().c_str(),
|
|
&startup.StartupInfo, &process)) return StartResult::failed;
|
|
Handle child(process.hProcess), thread(process.hThread);
|
|
// Prevent later child processes from inheriting our IPC and parent handles.
|
|
for (const auto handle : inherited) SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
|
|
const HANDLE wait_for[]{owned[ready].value, child.value};
|
|
if (WaitForMultipleObjects(2, wait_for, FALSE, 5000) != WAIT_OBJECT_0) {
|
|
SetEvent(owned[stopping].value);
|
|
return StartResult::failed;
|
|
}
|
|
for (std::size_t i = 0; i < handle_count; ++i) handles[i] = owned[i].release();
|
|
helper = child.release();
|
|
report = shared;
|
|
view.value = nullptr;
|
|
InterlockedExchange(&capturing, 0);
|
|
return StartResult::ready;
|
|
} catch (...) { return StartResult::failed; }
|
|
}
|
|
void capture(EXCEPTION_POINTERS* exception) noexcept {
|
|
if (!exception || InterlockedCompareExchange(&capturing, 1, 0) != 0) return;
|
|
report->thread_id = GetCurrentThreadId();
|
|
report->exception = exception;
|
|
MemoryBarrier();
|
|
if (SetEvent(handles[requested])) {
|
|
const HANDLE wait_for[]{handles[captured], helper};
|
|
WaitForMultipleObjects(2, wait_for, FALSE, capture_timeout_ms);
|
|
}
|
|
}
|
|
void stop() noexcept {
|
|
// An exception already using these handles owns them until process exit.
|
|
// Never unmap its report or close its events from a concurrent shutdown.
|
|
if (InterlockedCompareExchange(&capturing, 2, 0) != 0) return;
|
|
SetEvent(handles[stopping]);
|
|
UnmapViewOfFile(report);
|
|
report = nullptr;
|
|
for (auto& handle : handles) { CloseHandle(handle); handle = nullptr; }
|
|
CloseHandle(helper);
|
|
helper = nullptr;
|
|
}
|
|
}
|