/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "Crash.h" #ifdef USE_BREAKPAD #include #include #include #include #include #if defined(_WIN32) #include #include #include #include #else #error Breakpad support not implemented yet for this platform #endif #include "../Context.h" #include "../Game.h" #include "../GameState.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" #include "../Version.h" #include "../config/Config.h" #include "../core/Compression.h" #include "../core/Console.hpp" #include "../core/FileStream.h" #include "../core/Guard.hpp" #include "../core/Path.hpp" #include "../core/String.hpp" #include "../drawing/IDrawingEngine.h" #include "../interface/Screenshot.h" #include "../object/ObjectManager.h" #include "../park/ParkFile.h" #include "../sawyer_coding/SawyerCoding.h" #include "Platform.h" #define WSZ(x) L"" x #ifdef OPENRCT2_COMMIT_SHA1_SHORT static const wchar_t* _wszCommitSha1Short = WSZ(OPENRCT2_COMMIT_SHA1_SHORT); #else static const wchar_t* _wszCommitSha1Short = WSZ(""); #endif // OPENRCT2_ARCHITECTURE is required to be defined in version.h static const wchar_t* _wszArchitecture = WSZ(OPENRCT2_ARCHITECTURE); static std::map _uploadFiles; static std::mutex _uploadFilesMutex; #define BACKTRACE_TOKEN "97323bb36cc7c727b92d88a7f3554971fcd81166dfd427ecd671eeb4c369c64f" using namespace OpenRCT2; // Note: uploading gzipped crash dumps manually requires specifying // 'Content-Encoding: gzip' header in HTTP request, but we cannot do that, // so just hope the file name with '.gz' suffix is enough. // For docs on uploading to backtrace.io check // https://documentation.backtrace.io/product_integration_minidump_breakpad/ static bool UploadMinidump(const std::map& files, int& error, std::wstring& response) { for (const auto& file : files) { wprintf(L"files[%s] = %s\n", file.first.c_str(), file.second.c_str()); } std::wstring url( L"https://openrct2.sp.backtrace.io:6098/" L"post?format=minidump&token=" BACKTRACE_TOKEN); std::map parameters; parameters[L"product_name"] = L"openrct2"; parameters[L"version"] = String::toWideChar(gVersionInfoFull); // In case of releases this can be empty if (wcslen(_wszCommitSha1Short) > 0) { parameters[L"commit"] = _wszCommitSha1Short; } else { parameters[L"commit"] = String::toWideChar(gVersionInfoFull); } auto assertMsg = Guard::GetLastAssertMessage(); if (assertMsg.has_value()) { parameters[L"assert_failure"] = String::toWideChar(assertMsg.value()); } int timeout = 10000; bool success = google_breakpad::HTTPUpload::SendMultipartPostRequest(url, parameters, files, &timeout, &response, &error); wprintf(L"Success = %d, error = %d, response = %s\n", success, error, response.c_str()); return success; } static bool OnCrash( const wchar_t* dumpPath, const wchar_t* miniDumpId, void* context, EXCEPTION_POINTERS* exinfo, MDRawAssertionInfo* assertion, bool succeeded) { if (!succeeded) { constexpr const wchar_t* DumpFailedMessage = L"Failed to create the dump. Please file an issue with OpenRCT2 on GitHub " L"and provide latest save, and provide information about what you did " L"before the crash occurred."; wprintf(L"%ls\n", DumpFailedMessage); if (!gOpenRCT2SilentBreakpad) { MessageBoxW(nullptr, DumpFailedMessage, L"" OPENRCT2_NAME, MB_OK | MB_ICONERROR); } return succeeded; } // Get filenames wchar_t dumpFilePath[MAX_PATH]; wchar_t saveFilePath[MAX_PATH]; wchar_t configFilePath[MAX_PATH]; wchar_t recordFilePathNew[MAX_PATH]; swprintf_s(dumpFilePath, std::size(dumpFilePath), L"%s\\%s.dmp", dumpPath, miniDumpId); swprintf_s(saveFilePath, std::size(saveFilePath), L"%s\\%s.park", dumpPath, miniDumpId); swprintf_s(configFilePath, std::size(configFilePath), L"%s\\%s.ini", dumpPath, miniDumpId); swprintf_s(recordFilePathNew, std::size(recordFilePathNew), L"%s\\%s.parkrep", dumpPath, miniDumpId); wchar_t dumpFilePathNew[MAX_PATH]; swprintf_s( dumpFilePathNew, std::size(dumpFilePathNew), L"%s\\%s(%s_%s).dmp", dumpPath, miniDumpId, _wszCommitSha1Short, _wszArchitecture); wchar_t dumpFilePathGZIP[MAX_PATH]; swprintf_s(dumpFilePathGZIP, std::size(dumpFilePathGZIP), L"%s.gz", dumpFilePathNew); // Compress the dump { FileStream source(dumpFilePath, FileMode::open); FileStream dest(dumpFilePathGZIP, FileMode::write); // We could switch this to zstdCompress() if supported by backtrace.io. If you switch it, // use the extension .zst and ZstdMetadataType::both to use the appropriate metadata. if (Compression::zlibCompress(source, source.GetLength(), dest, Compression::ZlibHeaderType::gzip)) { // TODO: enable upload of gzip-compressed dumps once supported on // backtrace.io (uncomment the line below). For now leave compression // on, as GitHub will accept .gz files, even though it does not // advertise it officially. /* _uploadFiles[L"upload_file_minidump"] = dumpFilePathGZIP; */ } } bool with_record = StopSilentRecord(); // Try to rename the files if (_wrename(dumpFilePath, dumpFilePathNew) == 0) { std::wcscpy(dumpFilePath, dumpFilePathNew); } _uploadFiles[L"upload_file_minidump"] = dumpFilePath; // Compress to gzip-compatible stream // Log information to output wprintf(L"Dump Path: %s\n", dumpPath); wprintf(L"Dump File Path: %s\n", dumpFilePath); wprintf(L"Dump Id: %s\n", miniDumpId); wprintf(L"Version: %s\n", WSZ(kOpenRCT2Version)); wprintf(L"Commit: %s\n", _wszCommitSha1Short); bool savedGameDumped = false; auto saveFilePathUTF8 = String::toUtf8(saveFilePath); try { PrepareMapForSave(); // Export all loaded objects to avoid having custom objects missing in the reports. auto exporter = std::make_unique(); auto ctx = OpenRCT2::GetContext(); auto& objManager = ctx->GetObjectManager(); exporter->ExportObjectsList = objManager.GetPackableObjects(); auto& gameState = getGameState(); exporter->Export(gameState, saveFilePathUTF8.c_str(), kParkFileSaveCompressionLevel); savedGameDumped = true; } catch (const std::exception& e) { printf("Failed to export save. Error: %s\n", e.what()); } // Compress the save if (savedGameDumped) { _uploadFiles[L"attachment_park.park"] = saveFilePath; } auto configFilePathUTF8 = String::toUtf8(configFilePath); if (Config::SaveToPath(configFilePathUTF8)) { _uploadFiles[L"attachment_config.ini"] = configFilePath; } // janisozaur: https://github.com/OpenRCT2/OpenRCT2/pull/17634 // By the time we reach this point, OpenGL context is already lost causing *any* call to gl* to stall or fail in unexpected // way. Implementing a proof of concept with glGetGraphicsResetStatus in // https://github.com/OpenRCT2/OpenRCT2/commit/3974594fc36e24d14549921d378251242e3a23e2 yielded no additional information, // while potentially significantly raising the required OpenGL version. // There are (at least) two ways out of this: // 1. Create the screenshot with software renderer - requires allocations // 2. Not create screenshot at all. // Discovering which of the approaches got implemented is left as an excercise for the reader. if (OpenRCT2::GetContext()->GetDrawingEngineType() != DrawingEngine::OpenGL) { std::string screenshotPath = ScreenshotDump(); if (!screenshotPath.empty()) { auto screenshotPathW = String::toWideChar(screenshotPath.c_str()); _uploadFiles[L"attachment_screenshot.png"] = screenshotPathW; } } if (with_record) { auto parkReplayPathW = String::toWideChar(gSilentRecordingName); bool record_copied = CopyFileW(parkReplayPathW.c_str(), recordFilePathNew, true); if (record_copied) { _uploadFiles[L"attachment_replay.parkrep"] = recordFilePathNew; } else { with_record = false; } } if (gOpenRCT2SilentBreakpad) { printf("Uploading minidump in silent mode...\n"); int error; std::wstring response; UploadMinidump(_uploadFiles, error, response); return succeeded; } constexpr const wchar_t* MessageFormat = L"A crash has occurred and a dump was created at\n%s.\n\nPlease file an issue " L"with OpenRCT2 on GitHub, and provide " L"the dump and saved game there.\n\nVersion: %s\nCommit: %s\n\n" L"We would like to upload the crash dump for automated analysis, do you agree?\n" L"The automated analysis is done by courtesy of https://backtrace.io/"; wchar_t message[MAX_PATH * 2]; swprintf_s(message, MessageFormat, dumpFilePath, WSZ(kOpenRCT2Version), _wszCommitSha1Short); // Cannot use platform_show_messagebox here, it tries to set parent window already dead. int answer = MessageBoxW(nullptr, message, WSZ(OPENRCT2_NAME), MB_YESNO | MB_ICONERROR); if (answer == IDYES) { int error; std::wstring response; bool ok = UploadMinidump(_uploadFiles, error, response); if (!ok) { const wchar_t* MessageFormat2 = L"There was a problem while uploading the dump. Please upload it manually to " L"GitHub. It should be highlighted for you once you close this message.\n" L"It might be because you are using outdated build and we have disabled its " L"access token. Make sure you are running recent version.\n" L"Dump file = %s\n" L"Please provide following information as well:\n" L"Error code = %d\n" L"Response = %s"; swprintf_s(message, MessageFormat2, dumpFilePath, error, response.c_str()); MessageBoxW(nullptr, message, WSZ(OPENRCT2_NAME), MB_OK | MB_ICONERROR); } else { MessageBoxW(nullptr, L"Dump uploaded successfully.", WSZ(OPENRCT2_NAME), MB_OK | MB_ICONINFORMATION); } } HRESULT coInitializeResult = CoInitialize(nullptr); if (SUCCEEDED(coInitializeResult)) { LPITEMIDLIST pidl = ILCreateFromPathW(dumpPath); LPITEMIDLIST files[6]; uint32_t numFiles = 0; files[numFiles++] = ILCreateFromPathW(dumpFilePath); // There should be no need to check if this file exists, if it doesn't // it simply shouldn't get selected. files[numFiles++] = ILCreateFromPathW(dumpFilePathGZIP); files[numFiles++] = ILCreateFromPathW(configFilePath); if (savedGameDumped) { files[numFiles++] = ILCreateFromPathW(saveFilePath); } if (with_record) { files[numFiles++] = ILCreateFromPathW(recordFilePathNew); } if (pidl != nullptr) { SHOpenFolderAndSelectItems(pidl, numFiles, (LPCITEMIDLIST*)files, 0); ILFree(pidl); for (uint32_t i = 0; i < numFiles; i++) { ILFree(files[i]); } } CoUninitialize(); } // Return whether the dump was successful return succeeded; } static std::wstring GetDumpDirectory() { auto& env = GetContext()->GetPlatformEnvironment(); auto crashPath = env.GetDirectoryPath(DirBase::user, DirId::crashDumps); auto result = String::toWideChar(crashPath); return result; } // Using non-null pipe name here lets breakpad try setting OOP crash handling constexpr const wchar_t* PipeName = L"openrct2-bpad"; #endif // USE_BREAKPAD CExceptionHandler CrashInit() { #ifdef USE_BREAKPAD // Path must exist and be RW! auto exHandler = new google_breakpad::ExceptionHandler( GetDumpDirectory(), 0, OnCrash, 0, google_breakpad::ExceptionHandler::HANDLER_ALL, MiniDumpWithDataSegs, PipeName, 0); return reinterpret_cast(exHandler); #else // USE_BREAKPAD return nullptr; #endif // USE_BREAKPAD } void CrashRegisterAdditionalFile(const std::string& key, const std::string& path) { #ifdef USE_BREAKPAD std::lock_guard lock(_uploadFilesMutex); _uploadFiles[String::toWideChar(key.c_str())] = String::toWideChar(path.c_str()); #endif // USE_BREAKPAD } void CrashUnregisterAdditionalFile(const std::string& key) { #ifdef USE_BREAKPAD std::lock_guard lock(_uploadFilesMutex); auto it = _uploadFiles.find(String::toWideChar(key.c_str())); if (it != _uploadFiles.end()) { _uploadFiles.erase(it); } #endif // USE_BREAKPAD }