diff --git a/openrct2.common.props b/openrct2.common.props index c054fdb0fd8e..b3db3fed6173 100644 --- a/openrct2.common.props +++ b/openrct2.common.props @@ -61,7 +61,7 @@ /utf-8 /std:c++17 /permissive- /Zc:externConstexpr - imm32.lib;version.lib;winmm.lib;crypt32.lib;wldap32.lib;%(AdditionalDependencies) + wininet.lib;imm32.lib;version.lib;winmm.lib;crypt32.lib;wldap32.lib;%(AdditionalDependencies) /OPT:NOLBR /ignore:4099 %(AdditionalOptions) diff --git a/readme.md b/readme.md index a97afd2be097..07e0bafd14f6 100644 --- a/readme.md +++ b/readme.md @@ -219,7 +219,7 @@ We also need need scenarios to distribute with the game, when the time comes. Fo Companies that kindly allow us to use their stuff: -| DigitalOcean | JetBrains | AppVeyor | Travis-CI | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![do_logo_vertical_blue svg](https://user-images.githubusercontent.com/550290/36508276-8b572f0e-175c-11e8-8622-9febbce756b2.png)](https://www.digitalocean.com/) | [![jetbrains](https://user-images.githubusercontent.com/550290/36413299-0e0985ea-161e-11e8-8a01-3ef523b5905b.png)](https://www.jetbrains.com/) | [![AppVeyor](https://user-images.githubusercontent.com/550290/36508339-be413216-175c-11e8-97d8-760ced0931e8.png)](https://www.appveyor.com/) | [![Travis](https://raw.githubusercontent.com/travis-ci/docs-travis-ci-com/4b14eeab25ce8ca9164e177bfb60782a8535a822/images/travis-mascot-200px.png)](https://travis-ci.org/) | -| Hosting of various services | CLion and other products | MSVC CI | Linux + macOS CI | +| [DigitalOcean](https://www.digitalocean.com/) | [JetBrains](https://www.jetbrains.com/) | [AppVeyor](https://www.appveyor.com/) | [Travis-CI](https://travis-ci.org/) | [Backtrace](https://backtrace.io/) | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---| +| [![do_logo_vertical_blue svg](https://user-images.githubusercontent.com/550290/36508276-8b572f0e-175c-11e8-8622-9febbce756b2.png)](https://www.digitalocean.com/) | [![jetbrains](https://user-images.githubusercontent.com/550290/36413299-0e0985ea-161e-11e8-8a01-3ef523b5905b.png)](https://www.jetbrains.com/) | [![AppVeyor](https://user-images.githubusercontent.com/550290/36508339-be413216-175c-11e8-97d8-760ced0931e8.png)](https://www.appveyor.com/) | [![Travis](https://raw.githubusercontent.com/travis-ci/docs-travis-ci-com/4b14eeab25ce8ca9164e177bfb60782a8535a822/images/travis-mascot-200px.png)](https://travis-ci.org/) | [![backtrace](https://user-images.githubusercontent.com/550290/47113259-d0647680-d258-11e8-97c3-1a2c6bde6d11.png)](https://backtrace.io/) | +| Hosting of various services | CLion and other products | MSVC CI | Linux + macOS CI | Minidump uploads and inspection | diff --git a/scripts/ps/appveyor_deploy.ps1 b/scripts/ps/appveyor_deploy.ps1 index 2e5f47ca51b3..50c279e2b0db 100644 --- a/scripts/ps/appveyor_deploy.ps1 +++ b/scripts/ps/appveyor_deploy.ps1 @@ -14,6 +14,11 @@ if ($nottesting -and $notvs2015) if (${env:APPVEYOR_REPO_TAG} -eq "true" -or ${env:APPVEYOR_REPO_BRANCH} -match "^develop$|^push/") { msbuild openrct2.proj /t:UploadArtifacts /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" + # curl is sometimes aliased so be explicit + curl.exe --data-binary @bin/openrct2-dll.pdb 'https://openrct2.sp.backtrace.io:6098/post?format=symbols&token=e9e6d681fafdeac9f6131b4b59a155d54bebad567a8c0380d70643f4414819f5&upload_file=openrct2-dll.pdb' + curl.exe --data-binary @bin/openrct2-win.pdb 'https://openrct2.sp.backtrace.io:6098/post?format=symbols&token=e9e6d681fafdeac9f6131b4b59a155d54bebad567a8c0380d70643f4414819f5&upload_file=openrct2-win.pdb' + curl.exe --data-binary @bin/openrct2.dll 'https://openrct2.sp.backtrace.io:6098/post?format=symbols&token=e9e6d681fafdeac9f6131b4b59a155d54bebad567a8c0380d70643f4414819f5&upload_file=openrct2.dll' + curl.exe --data-binary @bin/openrct2.exe 'https://openrct2.sp.backtrace.io:6098/post?format=symbols&token=e9e6d681fafdeac9f6131b4b59a155d54bebad567a8c0380d70643f4414819f5&upload_file=openrct2.exe' } else { diff --git a/src/openrct2/platform/Crash.cpp b/src/openrct2/platform/Crash.cpp index 1d671d009193..c9d9fb2beab1 100644 --- a/src/openrct2/platform/Crash.cpp +++ b/src/openrct2/platform/Crash.cpp @@ -10,12 +10,14 @@ #include "Crash.h" #ifdef USE_BREAKPAD +# include # include # include # if defined(_WIN32) # include # include +# include # include # else # error Breakpad support not implemented yet for this platform @@ -23,9 +25,11 @@ # include "../Version.h" # include "../core/Console.hpp" +# include "../core/String.hpp" # include "../localisation/Language.h" # include "../rct2/S6Exporter.h" # include "../scenario/Scenario.h" +# include "../util/Util.h" # include "platform.h" # define WSZ(x) L"" x @@ -39,6 +43,34 @@ const wchar_t* _wszCommitSha1Short = WSZ(""); // OPENRCT2_ARCHITECTURE is required to be defined in version.h const wchar_t* _wszArchitecture = WSZ(OPENRCT2_ARCHITECTURE); +// 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 uplading to backtrace.io check +// https://documentation.backtrace.io/product_integration_minidump_breakpad/ +static bool UploadMinidump(const wchar_t* dumpPath, int& error, std::wstring& response) +{ + std::wstring url(L"https://openrct2.sp.backtrace.io:6098/" + L"post?format=minidump&token=f9c5e640d498f15dbe902eab3e822e472af9270d5b0cbdc269cee65a926bf306"); + std::map parameters; + std::map files; + parameters[L"product_name"] = L"openrct2"; + // In case of releases this can be empty + if (wcslen(_wszCommitSha1Short) > 0) + { + parameters[L"commit"] = _wszCommitSha1Short; + } + else + { + parameters[L"commit"] = String::ToUtf16(gVersionInfoFull); + } + files[L"upload_file_minidump"] = dumpPath; + int timeout = 10000; + bool success = google_breakpad::HTTPUpload::SendRequest(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) @@ -59,19 +91,39 @@ static bool OnCrash( // Get filenames wchar_t dumpFilePath[MAX_PATH]; wchar_t saveFilePath[MAX_PATH]; - swprintf_s(dumpFilePath, sizeof(dumpFilePath), L"%s/%s.dmp", dumpPath, miniDumpId); - swprintf_s(saveFilePath, sizeof(saveFilePath), L"%s/%s.sv6", dumpPath, miniDumpId); + swprintf_s(dumpFilePath, sizeof(dumpFilePath), L"%s\\%s.dmp", dumpPath, miniDumpId); + swprintf_s(saveFilePath, sizeof(saveFilePath), L"%s\\%s.sv6", dumpPath, miniDumpId); + const wchar_t* minidumpToUpload = dumpFilePath; - // Try to rename the files wchar_t dumpFilePathNew[MAX_PATH]; swprintf_s( - dumpFilePathNew, sizeof(dumpFilePathNew), L"%s/%s(%s_%s).dmp", dumpPath, miniDumpId, _wszCommitSha1Short, + dumpFilePathNew, sizeof(dumpFilePathNew), L"%s\\%s(%s_%s).dmp", dumpPath, miniDumpId, _wszCommitSha1Short, _wszArchitecture); + + wchar_t dumpFilePathGZIP[MAX_PATH]; + swprintf_s(dumpFilePathGZIP, sizeof(dumpFilePathGZIP), L"%s.gz", dumpFilePathNew); + + // TODO: enable gzip compression once supported on backtrace.io + /* + FILE* input = _wfopen(dumpFilePath, L"rb"); + FILE* dest = _wfopen(dumpFilePathGZIP, L"wb"); + + if (util_gzip_compress(input, dest)) + { + minidumpToUpload = dumpFilePathGZIP; + } + fclose(input); + fclose(dest); + */ + + // Try to rename the files if (_wrename(dumpFilePath, dumpFilePathNew) == 0) { std::wcscpy(dumpFilePath, dumpFilePathNew); } + // Compress to gzip-compatible stream + // Log information to output wprintf(L"Dump Path: %s\n", dumpPath); wprintf(L"Dump File Path: %s\n", dumpFilePath); @@ -95,25 +147,53 @@ static bool OnCrash( if (gOpenRCT2SilentBreakpad) { + int error; + std::wstring response; + UploadMinidump(minidumpToUpload, 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"; + 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. - MessageBoxW(nullptr, message, WSZ(OPENRCT2_NAME), MB_OK | MB_ICONERROR); + int answer = MessageBoxW(nullptr, message, WSZ(OPENRCT2_NAME), MB_YESNO | MB_ICONERROR); + if (answer == IDYES) + { + int error; + std::wstring response; + bool ok = UploadMinidump(minidumpToUpload, 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"Please provide following information as well:\n" + L"Error code = %d\n" + L"Response = %s"; + swprintf_s(message, MessageFormat2, error, response.c_str()); + MessageBoxW(nullptr, message, WSZ(OPENRCT2_NAME), MB_OK | MB_ICONERROR); + } + else + { + MessageBoxW(nullptr, L"Dump uploaded succesfully.", WSZ(OPENRCT2_NAME), MB_OK | MB_ICONINFORMATION); + } + } HRESULT coInitializeResult = CoInitialize(nullptr); if (SUCCEEDED(coInitializeResult)) { LPITEMIDLIST pidl = ILCreateFromPathW(dumpPath); - LPITEMIDLIST files[2]; + LPITEMIDLIST files[3]; 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); if (savedGameDumped) { files[numFiles++] = ILCreateFromPathW(saveFilePath); diff --git a/src/openrct2/util/Util.cpp b/src/openrct2/util/Util.cpp index ff58a84a42e5..500b83e0b5f6 100644 --- a/src/openrct2/util/Util.cpp +++ b/src/openrct2/util/Util.cpp @@ -630,6 +630,64 @@ uint8_t* util_zlib_deflate(const uint8_t* data, size_t data_in_size, size_t* dat return buffer; } +// Compress the source to gzip-compatible stream, write to dest. +// Mainly used for compressing the crashdumps +bool util_gzip_compress(FILE* source, FILE* dest) +{ + if (source == nullptr || dest == nullptr) + { + return false; + } + int ret, flush; + size_t have; + z_stream strm{}; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + unsigned char in[CHUNK]; + unsigned char out[CHUNK]; + int windowBits = 15; + int GZIP_ENCODING = 16; + ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, windowBits | GZIP_ENCODING, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) + { + log_error("Failed to initialise stream"); + return false; + } + do + { + strm.avail_in = uInt(fread(in, 1, CHUNK, source)); + if (ferror(source)) + { + deflateEnd(&strm); + log_error("Failed to read data from source"); + return false; + } + flush = feof(source) ? Z_FINISH : Z_NO_FLUSH; + strm.next_in = in; + do + { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = deflate(&strm, flush); + if (ret == Z_STREAM_ERROR) + { + log_error("Failed to compress data"); + return false; + } + have = CHUNK - strm.avail_out; + if (fwrite(out, 1, have, dest) != have || ferror(dest)) + { + deflateEnd(&strm); + log_error("Failed to write data to destination"); + return false; + } + } while (strm.avail_out == 0); + } while (flush != Z_FINISH); + deflateEnd(&strm); + return true; +} + // Type-independent code left as macro to reduce duplicate code. #define add_clamp_body(value, value_to_add, min_cap, max_cap) \ if ((value_to_add > 0) && (value > (max_cap - (value_to_add)))) \ diff --git a/src/openrct2/util/Util.h b/src/openrct2/util/Util.h index 462e46308ab1..d8c45030e2a3 100644 --- a/src/openrct2/util/Util.h +++ b/src/openrct2/util/Util.h @@ -12,7 +12,8 @@ #include "../common.h" -#include +#include +#include int32_t squaredmetres_to_squaredfeet(int32_t squaredMetres); int32_t metres_to_feet(int32_t metres); @@ -56,6 +57,7 @@ uint32_t util_rand(); uint8_t* util_zlib_deflate(const uint8_t* data, size_t data_in_size, size_t* data_out_size); uint8_t* util_zlib_inflate(uint8_t* data, size_t data_in_size, size_t* data_out_size); +bool util_gzip_compress(FILE* source, FILE* dest); int8_t add_clamp_int8_t(int8_t value, int8_t value_to_add); int16_t add_clamp_int16_t(int16_t value, int16_t value_to_add);