/***************************************************************************** * Copyright (c) 2014-2023 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 # 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 "../OpenRCT2.h" # include "../PlatformEnvironment.h" # include "../Version.h" # include "../config/Config.h" # include "../core/Console.hpp" # include "../core/Guard.hpp" # include "../core/Path.hpp" # include "../core/String.hpp" # include "../drawing/IDrawingEngine.h" # include "../interface/Screenshot.h" # include "../localisation/Language.h" # include "../object/ObjectManager.h" # include "../park/ParkFile.h" # include "../scenario/Scenario.h" # include "../util/SawyerCoding.h" # include "../util/Util.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; # define BACKTRACE_TOKEN L"d3de8c689ea81ff21e030f1afeea5e293c0be6bfaae6975cd353c23fe6948322" 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 { FILE* input = _wfopen(dumpFilePath, L"rb"); FILE* dest = _wfopen(dumpFilePathGZIP, L"wb"); if (UtilGzipCompress(input, dest)) { // 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; */ } fclose(input); fclose(dest); } 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(OPENRCT2_VERSION)); 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(); exporter->Export(saveFilePathUTF8.c_str()); 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 (ConfigSave(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(OPENRCT2_VERSION), _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::CRASH); 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 _uploadFiles[String::ToWideChar(key.c_str())] = String::ToWideChar(path.c_str()); #endif // USE_BREAKPAD } void CrashUnregisterAdditionalFile(const std::string& key) { #ifdef USE_BREAKPAD auto it = _uploadFiles.find(String::ToWideChar(key.c_str())); if (it != _uploadFiles.end()) { _uploadFiles.erase(it); } #endif // USE_BREAKPAD }