From 6a4a89a73e04dc6e52b97f5a18048b49b7954d44 Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Sun, 27 Aug 2023 12:16:50 +0200 Subject: [PATCH] Change: store crash logs in JSON format --- src/crashlog.cpp | 392 +++++++++----------------------- src/crashlog.h | 48 ++-- src/network/network_survey.cpp | 3 + src/os/macosx/crashlog_osx.cpp | 45 +--- src/os/unix/crashlog_unix.cpp | 43 +--- src/os/windows/crashlog_win.cpp | 102 +++++---- src/survey.cpp | 135 +++++++++++ src/survey.h | 2 + 8 files changed, 338 insertions(+), 432 deletions(-) diff --git a/src/crashlog.cpp b/src/crashlog.cpp index 1ea5b877ac..11ec63f9a3 100644 --- a/src/crashlog.cpp +++ b/src/crashlog.cpp @@ -9,297 +9,60 @@ #include "stdafx.h" #include "crashlog.h" +#include "survey.h" #include "gamelog.h" -#include "timer/timer_game_calendar.h" #include "map_func.h" -#include "rev.h" -#include "strings_func.h" -#include "blitter/factory.hpp" -#include "base_media_base.h" #include "music/music_driver.hpp" #include "sound/sound_driver.hpp" #include "video/video_driver.hpp" #include "saveload/saveload.h" #include "screenshot.h" -#include "gfx_func.h" -#include "network/network.h" #include "network/network_survey.h" -#include "language.h" -#include "fontcache.h" #include "news_gui.h" +#include "fileio_func.h" +#include "fileio_type.h" -#include "ai/ai_info.hpp" -#include "game/game.hpp" -#include "game/game_info.hpp" -#include "company_base.h" #include "company_func.h" #include "3rdparty/fmt/chrono.h" #include "3rdparty/fmt/std.h" - -#ifdef WITH_ALLEGRO -# include -#endif /* WITH_ALLEGRO */ -#ifdef WITH_FONTCONFIG -# include -#endif /* WITH_FONTCONFIG */ -#ifdef WITH_PNG - /* pngconf.h, included by png.h doesn't like something in the - * freetype headers. As such it's not alphabetically sorted. */ -# include -#endif /* WITH_PNG */ -#ifdef WITH_FREETYPE -# include -# include FT_FREETYPE_H -#endif /* WITH_FREETYPE */ -#ifdef WITH_HARFBUZZ -# include -#endif /* WITH_HARFBUZZ */ -#ifdef WITH_ICU_I18N -# include -#endif /* WITH_ICU_I18N */ -#ifdef WITH_LIBLZMA -# include -#endif -#ifdef WITH_LZO -#include -#endif -#if defined(WITH_SDL) || defined(WITH_SDL2) -# include -#endif /* WITH_SDL || WITH_SDL2 */ -#ifdef WITH_ZLIB -# include -#endif -#ifdef WITH_CURL -# include -#endif +#include "core/format.hpp" #include "safeguards.h" -/* static */ std::string CrashLog::message{ "" }; +/* static */ std::string CrashLog::message{}; -void CrashLog::LogCompiler(std::back_insert_iterator &output_iterator) const -{ - fmt::format_to(output_iterator, " Compiler: " -#if defined(_MSC_VER) - "MSVC {}", _MSC_VER -#elif defined(__ICC) && defined(__GNUC__) - "ICC {} (GCC {}.{}.{} mode)", __ICC, __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__ -#elif defined(__ICC) - "ICC {}", __ICC -#elif defined(__GNUC__) - "GCC {}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__ -#else - "" -#endif - ); -#if defined(__VERSION__) - fmt::format_to(output_iterator, " \"" __VERSION__ "\"\n\n"); -#else - fmt::format_to(output_iterator, "\n\n"); -#endif -} - -/** - * Writes OpenTTD's version to the buffer. - * @param output_iterator Iterator to write the output to. - */ -void CrashLog::LogOpenTTDVersion(std::back_insert_iterator &output_iterator) const -{ - fmt::format_to(output_iterator, - "OpenTTD version:\n" - " Version: {} ({})\n" - " NewGRF ver: {:08x}\n" - " Bits: {}\n" - " Endian: {}\n" - " Dedicated: {}\n" - " Build date: {}\n\n", - _openttd_revision, - _openttd_revision_modified, - _openttd_newgrf_version, -#ifdef POINTER_IS_64BIT - 64, -#else - 32, -#endif -#if (TTD_ENDIAN == TTD_LITTLE_ENDIAN) - "little", -#else - "big", -#endif -#ifdef DEDICATED - "yes", -#else - "no", -#endif - _openttd_build_date - ); -} - -/** - * Writes the (important) configuration settings to the buffer. - * E.g. graphics set, sound set, blitter and AIs. - * @param output_iterator Iterator to write the output to. - */ -void CrashLog::LogConfiguration(std::back_insert_iterator &output_iterator) const -{ - fmt::format_to(output_iterator, - "Configuration:\n" - " Blitter: {}\n" - " Graphics set: {} ({})\n" - " Language: {}\n" - " Music driver: {}\n" - " Music set: {} ({})\n" - " Network: {}\n" - " Sound driver: {}\n" - " Sound set: {} ({})\n" - " Video driver: {}\n\n", - BlitterFactory::GetCurrentBlitter() == nullptr ? "none" : BlitterFactory::GetCurrentBlitter()->GetName(), - BaseGraphics::GetUsedSet() == nullptr ? "none" : BaseGraphics::GetUsedSet()->name, - BaseGraphics::GetUsedSet() == nullptr ? UINT32_MAX : BaseGraphics::GetUsedSet()->version, - _current_language == nullptr ? "none" : _current_language->file.filename(), - MusicDriver::GetInstance() == nullptr ? "none" : MusicDriver::GetInstance()->GetName(), - BaseMusic::GetUsedSet() == nullptr ? "none" : BaseMusic::GetUsedSet()->name, - BaseMusic::GetUsedSet() == nullptr ? UINT32_MAX : BaseMusic::GetUsedSet()->version, - _networking ? (_network_server ? "server" : "client") : "no", - SoundDriver::GetInstance() == nullptr ? "none" : SoundDriver::GetInstance()->GetName(), - BaseSounds::GetUsedSet() == nullptr ? "none" : BaseSounds::GetUsedSet()->name, - BaseSounds::GetUsedSet() == nullptr ? UINT32_MAX : BaseSounds::GetUsedSet()->version, - VideoDriver::GetInstance() == nullptr ? "none" : VideoDriver::GetInstance()->GetInfoString() - ); - - fmt::format_to(output_iterator, - "Fonts:\n" - " Small: {}\n" - " Medium: {}\n" - " Large: {}\n" - " Mono: {}\n\n", - FontCache::GetName(FS_SMALL), - FontCache::GetName(FS_NORMAL), - FontCache::GetName(FS_LARGE), - FontCache::GetName(FS_MONO) - ); - - fmt::format_to(output_iterator, "AI Configuration (local: {}) (current: {}):\n", _local_company, _current_company); - for (const Company *c : Company::Iterate()) { - if (c->ai_info == nullptr) { - fmt::format_to(output_iterator, " {:2}: Human\n", c->index); - } else { - fmt::format_to(output_iterator, " {:2}: {} (v{})\n", (int)c->index, c->ai_info->GetName(), c->ai_info->GetVersion()); - } - } - - if (Game::GetInfo() != nullptr) { - fmt::format_to(output_iterator, " GS: {} (v{})\n", Game::GetInfo()->GetName(), Game::GetInfo()->GetVersion()); - } - fmt::format_to(output_iterator, "\n"); -} - -/** - * Writes information (versions) of the used libraries. - * @param output_iterator Iterator to write the output to. - */ -void CrashLog::LogLibraries(std::back_insert_iterator &output_iterator) const -{ - fmt::format_to(output_iterator, "Libraries:\n"); - -#ifdef WITH_ALLEGRO - fmt::format_to(output_iterator, " Allegro: {}\n", allegro_id); -#endif /* WITH_ALLEGRO */ - -#ifdef WITH_FONTCONFIG - int version = FcGetVersion(); - fmt::format_to(output_iterator, " FontConfig: {}.{}.{}\n", version / 10000, (version / 100) % 100, version % 100); -#endif /* WITH_FONTCONFIG */ - -#ifdef WITH_FREETYPE - FT_Library library; - int major, minor, patch; - FT_Init_FreeType(&library); - FT_Library_Version(library, &major, &minor, &patch); - FT_Done_FreeType(library); - fmt::format_to(output_iterator, " FreeType: {}.{}.{}\n", major, minor, patch); -#endif /* WITH_FREETYPE */ - -#if defined(WITH_HARFBUZZ) - fmt::format_to(output_iterator, " HarfBuzz: {}\n", hb_version_string()); -#endif /* WITH_HARFBUZZ */ - -#if defined(WITH_ICU_I18N) - /* 4 times 0-255, separated by dots (.) and a trailing '\0' */ - char buf[4 * 3 + 3 + 1]; - UVersionInfo ver; - u_getVersion(ver); - u_versionToString(ver, buf); - fmt::format_to(output_iterator, " ICU i18n: {}\n", buf); -#endif /* WITH_ICU_I18N */ - -#ifdef WITH_LIBLZMA - fmt::format_to(output_iterator, " LZMA: {}\n", lzma_version_string()); -#endif - -#ifdef WITH_LZO - fmt::format_to(output_iterator, " LZO: {}\n", lzo_version_string()); -#endif - -#ifdef WITH_PNG - fmt::format_to(output_iterator, " PNG: {}\n", png_get_libpng_ver(nullptr)); -#endif /* WITH_PNG */ - -#ifdef WITH_SDL - const SDL_version *sdl_v = SDL_Linked_Version(); - fmt::format_to(output_iterator, " SDL1: {}.{}.{}\n", sdl_v->major, sdl_v->minor, sdl_v->patch); -#elif defined(WITH_SDL2) - SDL_version sdl2_v; - SDL_GetVersion(&sdl2_v); - fmt::format_to(output_iterator, " SDL2: {}.{}.{}\n", sdl2_v.major, sdl2_v.minor, sdl2_v.patch); -#endif - -#ifdef WITH_ZLIB - fmt::format_to(output_iterator, " Zlib: {}\n", zlibVersion()); -#endif - -#ifdef WITH_CURL - auto *curl_v = curl_version_info(CURLVERSION_NOW); - fmt::format_to(output_iterator, " Curl: {}\n", curl_v->version); - if (curl_v->ssl_version != nullptr) { - fmt::format_to(output_iterator, " Curl SSL: {}\n", curl_v->ssl_version); - } else { - fmt::format_to(output_iterator, " Curl SSL: none\n"); - } -#endif - - fmt::format_to(output_iterator, "\n"); -} +/** The version of the schema of the JSON information. */ +constexpr uint8_t CRASHLOG_SURVEY_VERSION = 1; /** * Writes the gamelog data to the buffer. * @param output_iterator Iterator to write the output to. */ -void CrashLog::LogGamelog(std::back_insert_iterator &output_iterator) const +static void SurveyGamelog(nlohmann::json &json) { - _gamelog.Print([&output_iterator](const std::string &s) { - fmt::format_to(output_iterator, "{}\n", s); + json = nlohmann::json::array(); + + _gamelog.Print([&json](const std::string &s) { + json.push_back(s); }); - fmt::format_to(output_iterator, "\n"); } /** * Writes up to 32 recent news messages to the buffer, with the most recent first. * @param output_iterator Iterator to write the output to. */ -void CrashLog::LogRecentNews(std::back_insert_iterator &output_iterator) const +static void SurveyRecentNews(nlohmann::json &json) { - fmt::format_to(output_iterator, "Recent news messages:\n"); + json = nlohmann::json::array(); int i = 0; for (NewsItem *news = _latest_news; i < 32 && news != nullptr; news = news->prev, i++) { TimerGameCalendar::YearMonthDay ymd; TimerGameCalendar::ConvertDateToYMD(news->date, &ymd); - fmt::format_to(output_iterator, "({}-{:02}-{:02}) StringID: {}, Type: {}, Ref1: {}, {}, Ref2: {}, {}\n", - ymd.year, ymd.month + 1, ymd.day, news->string_id, news->type, - news->reftype1, news->ref1, news->reftype2, news->ref2); + json.push_back(fmt::format("({}-{:02}-{:02}) StringID: {}, Type: {}, Ref1: {}, {}, Ref2: {}, {}", + ymd.year, ymd.month + 1, ymd.day, news->string_id, news->type, + news->reftype1, news->ref1, news->reftype2, news->ref2)); } - fmt::format_to(output_iterator, "\n"); } /** @@ -320,28 +83,95 @@ std::string CrashLog::CreateFileName(const char *ext, bool with_dir) const /** * Fill the crash log buffer with all data of a crash log. - * @param output_iterator Iterator to write the output to. */ -void CrashLog::FillCrashLog(std::back_insert_iterator &output_iterator) const +void CrashLog::FillCrashLog() { - fmt::format_to(output_iterator, "*** OpenTTD Crash Report ***\n\n"); - fmt::format_to(output_iterator, "Crash at: {:%Y-%m-%d %H:%M:%S} (UTC)\n", fmt::gmtime(time(nullptr))); + /* Reminder: this JSON is read in an automated fashion. + * If any structural changes are applied, please bump the version. */ + this->survey["schema"] = CRASHLOG_SURVEY_VERSION; + this->survey["date"] = fmt::format("{:%Y-%m-%d %H:%M:%S} (UTC)", fmt::gmtime(time(nullptr))); - TimerGameCalendar::YearMonthDay ymd; - TimerGameCalendar::ConvertDateToYMD(TimerGameCalendar::date, &ymd); - fmt::format_to(output_iterator, "In game date: {}-{:02}-{:02} ({})\n\n", ymd.year, ymd.month + 1, ymd.day, TimerGameCalendar::date_fract); + /* If no internal reason was logged, it must be a crash. */ + if (CrashLog::message.empty()) { + this->SurveyCrash(this->survey["crash"]); + } else { + this->survey["crash"]["reason"] = CrashLog::message; + CrashLog::message.clear(); + } - this->LogError(output_iterator, CrashLog::message); - this->LogOpenTTDVersion(output_iterator); - this->LogStacktrace(output_iterator); - this->LogOSVersion(output_iterator); - this->LogCompiler(output_iterator); - this->LogConfiguration(output_iterator); - this->LogLibraries(output_iterator); - this->LogGamelog(output_iterator); - this->LogRecentNews(output_iterator); + if (!this->TryExecute("stacktrace", [this]() { this->SurveyStacktrace(this->survey["stacktrace"]); return true; })) { + this->survey["stacktrace"] = "crashed while gathering information"; + } - fmt::format_to(output_iterator, "*** End of OpenTTD Crash Report ***\n"); + { + auto &info = this->survey["info"]; + if (!this->TryExecute("os", [&info]() { SurveyOS(info["os"]); return true; })) { + info["os"] = "crashed while gathering information"; + } + if (!this->TryExecute("openttd", [&info]() { SurveyOpenTTD(info["openttd"]); return true; })) { + info["openttd"] = "crashed while gathering information"; + } + if (!this->TryExecute("configuration", [&info]() { SurveyConfiguration(info["configuration"]); return true; })) { + info["configuration"] = "crashed while gathering information"; + } + if (!this->TryExecute("font", [&info]() { SurveyFont(info["font"]); return true; })) { + info["font"] = "crashed while gathering information"; + } + if (!this->TryExecute("compiler", [&info]() { SurveyCompiler(info["compiler"]); return true; })) { + info["compiler"] = "crashed while gathering information"; + } + if (!this->TryExecute("libraries", [&info]() { SurveyLibraries(info["libraries"]); return true; })) { + info["libraries"] = "crashed while gathering information"; + } + } + + { + auto &game = this->survey["game"]; + game["local_company"] = _local_company; + game["current_company"] = _current_company; + + if (!this->TryExecute("timers", [&game]() { SurveyTimers(game["timers"]); return true; })) { + game["libraries"] = "crashed while gathering information"; + } + if (!this->TryExecute("companies", [&game]() { SurveyCompanies(game["companies"]); return true; })) { + game["companies"] = "crashed while gathering information"; + } + if (!this->TryExecute("settings", [&game]() { SurveySettings(game["settings"]); return true; })) { + game["settings"] = "crashed while gathering information"; + } + if (!this->TryExecute("grfs", [&game]() { SurveyGrfs(game["grfs"]); return true; })) { + game["grfs"] = "crashed while gathering information"; + } + if (!this->TryExecute("game_script", [&game]() { SurveyGameScript(game["game_script"]); return true; })) { + game["game_script"] = "crashed while gathering information"; + } + if (!this->TryExecute("gamelog", [&game]() { SurveyGamelog(game["gamelog"]); return true; })) { + game["gamelog"] = "crashed while gathering information"; + } + if (!this->TryExecute("news", [&game]() { SurveyRecentNews(game["news"]); return true; })) { + game["news"] = "crashed while gathering information"; + } + } +} + +void CrashLog::PrintCrashLog() const +{ + fmt::print(" OpenTTD version:\n"); + fmt::print(" Version: {}\n", this->survey["info"]["openttd"]["version"]["revision"].get()); + fmt::print(" Hash: {}\n", this->survey["info"]["openttd"]["version"]["hash"].get()); + fmt::print(" NewGRF ver: {}\n", this->survey["info"]["openttd"]["version"]["newgrf"].get()); + fmt::print(" Content ver: {}\n", this->survey["info"]["openttd"]["version"]["content"].get()); + fmt::print("\n"); + + fmt::print(" Crash:\n"); + fmt::print(" Reason: {}\n", this->survey["crash"]["reason"].get()); + fmt::print("\n"); + + fmt::print(" Stacktrace:\n"); + for (const auto &line : this->survey["stacktrace"]) { + fmt::print(" {}\n", line.get()); + } + fmt::print("\n"); } /** @@ -351,13 +181,15 @@ void CrashLog::FillCrashLog(std::back_insert_iterator &output_itera */ bool CrashLog::WriteCrashLog() { - this->crashlog_filename = this->CreateFileName(".log"); + this->crashlog_filename = this->CreateFileName(".json.log"); FILE *file = FioFOpenFile(this->crashlog_filename, "w", NO_DIRECTORY); if (file == nullptr) return false; - size_t len = this->crashlog.size(); - size_t written = fwrite(this->crashlog.data(), 1, len, file); + std::string survey_json = this->survey.dump(4); + + size_t len = survey_json.size(); + size_t written = fwrite(survey_json.data(), 1, len, file); FioFCloseFile(file); return len == written; @@ -414,6 +246,9 @@ bool CrashLog::WriteScreenshot() return res; } +/** + * Send the survey result, noting it was a crash. + */ void CrashLog::SendSurvey() const { if (_game_mode == GM_NORMAL) { @@ -433,14 +268,13 @@ void CrashLog::MakeCrashLog() if (crashlogged) return; crashlogged = true; - crashlog.reserve(65536); - auto output_iterator = std::back_inserter(crashlog); - fmt::print("Crash encountered, generating crash log...\n"); - this->FillCrashLog(output_iterator); - fmt::print("{}\n", crashlog); + this->FillCrashLog(); fmt::print("Crash log generated.\n\n"); + fmt::print("Crash in summary:\n"); + this->TryExecute("crashlog", [this]() { this->PrintCrashLog(); return true; }); + fmt::print("Writing crash log to disk...\n"); bool ret = this->TryExecute("crashlog", [this]() { return this->WriteCrashLog(); }); if (ret) { diff --git a/src/crashlog.h b/src/crashlog.h index 2bff7cf2cc..836bef5082 100644 --- a/src/crashlog.h +++ b/src/crashlog.h @@ -10,6 +10,8 @@ #ifndef CRASHLOG_H #define CRASHLOG_H +#include "3rdparty/nlohmann/json.hpp" + /** * Helper class for creating crash logs. */ @@ -17,40 +19,20 @@ class CrashLog { private: /** Error message coming from #FatalError(format, ...). */ static std::string message; -protected: - /** - * Writes OS' version to the buffer. - * @param output_iterator Iterator to write the output to. - */ - virtual void LogOSVersion(std::back_insert_iterator &output_iterator) const = 0; /** - * Writes compiler (and its version, if available) to the buffer. - * @param output_iterator Iterator to write the output to. + * Convert system crash reason to JSON. + * + * @param survey The JSON object. */ - virtual void LogCompiler(std::back_insert_iterator &output_iterator) const; + virtual void SurveyCrash(nlohmann::json &survey) const = 0; /** - * Writes actually encountered error to the buffer. - * @param output_iterator Iterator to write the output to. - * @param message Message passed to use for errors. + * Convert stacktrace to JSON. + * + * @param survey The JSON object. */ - virtual void LogError(std::back_insert_iterator &output_iterator, const std::string_view message) const = 0; - - /** - * Writes the stack trace to the buffer, if there is information about it - * available. - * @param output_iterator Iterator to write the output to. - */ - virtual void LogStacktrace(std::back_insert_iterator &output_iterator) const = 0; - - void LogOpenTTDVersion(std::back_insert_iterator &output_iterator) const; - void LogConfiguration(std::back_insert_iterator &output_iterator) const; - void LogLibraries(std::back_insert_iterator &output_iterator) const; - void LogGamelog(std::back_insert_iterator &output_iterator) const; - void LogRecentNews(std::back_insert_iterator &output_iterator) const; - - std::string CreateFileName(const char *ext, bool with_dir = true) const; + virtual void SurveyStacktrace(nlohmann::json &survey) const = 0; /** * Execute the func() and return its value. If any exception / signal / crash happens, @@ -63,19 +45,23 @@ protected: */ virtual bool TryExecute(std::string_view section_name, std::function &&func) = 0; +protected: + std::string CreateFileName(const char *ext, bool with_dir = true) const; + public: /** Stub destructor to silence some compilers. */ virtual ~CrashLog() = default; - std::string crashlog; + nlohmann::json survey; std::string crashlog_filename; std::string crashdump_filename; std::string savegame_filename; std::string screenshot_filename; - void FillCrashLog(std::back_insert_iterator &output_iterator) const; - bool WriteCrashLog(); + void FillCrashLog(); + void PrintCrashLog() const; + bool WriteCrashLog(); virtual bool WriteCrashDump(); bool WriteSavegame(); bool WriteScreenshot(); diff --git a/src/network/network_survey.cpp b/src/network/network_survey.cpp index 9a83edfecb..dab745d078 100644 --- a/src/network/network_survey.cpp +++ b/src/network/network_survey.cpp @@ -43,6 +43,7 @@ std::string NetworkSurveyHandler::CreatePayload(Reason reason, bool for_preview) survey["schema"] = NETWORK_SURVEY_VERSION; survey["reason"] = reason; survey["id"] = _savegame_id; + survey["date"] = fmt::format("{:%Y-%m-%d %H:%M:%S} (UTC)", fmt::gmtime(time(nullptr))); #ifdef SURVEY_KEY /* We censor the key to avoid people trying to be "clever" and use it to send their own surveys. */ @@ -57,6 +58,8 @@ std::string NetworkSurveyHandler::CreatePayload(Reason reason, bool for_preview) SurveyOpenTTD(info["openttd"]); SurveyConfiguration(info["configuration"]); SurveyFont(info["font"]); + SurveyCompiler(info["compiler"]); + SurveyLibraries(info["libraries"]); } { diff --git a/src/os/macosx/crashlog_osx.cpp b/src/os/macosx/crashlog_osx.cpp index f285bbd0e9..391a508143 100644 --- a/src/os/macosx/crashlog_osx.cpp +++ b/src/os/macosx/crashlog_osx.cpp @@ -49,53 +49,24 @@ class CrashLogOSX : public CrashLog { /** Signal that has been thrown. */ int signum; - void LogOSVersion(std::back_insert_iterator &output_iterator) const override + void SurveyCrash(nlohmann::json &survey) const override { - int ver_maj, ver_min, ver_bug; - GetMacOSVersion(&ver_maj, &ver_min, &ver_bug); - - const NXArchInfo *arch = NXGetLocalArchInfo(); - - fmt::format_to(output_iterator, - "Operating system:\n" - " Name: Mac OS X\n" - " Release: {}.{}.{}\n" - " Machine: {}\n" - " Min Ver: {}\n" - " Max Ver: {}\n", - ver_maj, ver_min, ver_bug, - arch != nullptr ? arch->description : "unknown", - MAC_OS_X_VERSION_MIN_REQUIRED, - MAC_OS_X_VERSION_MAX_ALLOWED - ); + survey["id"] = signum; + survey["reason"] = strsignal(signum); } - void LogError(std::back_insert_iterator &output_iterator, const std::string_view message) const override + void SurveyStacktrace(nlohmann::json &survey) const override { - fmt::format_to(output_iterator, - "Crash reason:\n" - " Signal: {} ({})\n" - " Message: {}\n\n", - strsignal(this->signum), - this->signum, - message - ); - } - - void LogStacktrace(std::back_insert_iterator &output_iterator) const override - { - fmt::format_to(output_iterator, "\nStacktrace:\n"); - void *trace[64]; int trace_size = backtrace(trace, lengthof(trace)); + survey = nlohmann::json::array(); + char **messages = backtrace_symbols(trace, trace_size); for (int i = 0; i < trace_size; i++) { - fmt::format_to(output_iterator, "{}\n", messages[i]); + survey.push_back(messages[i]); } free(messages); - - fmt::format_to(output_iterator, "\n"); } #ifdef WITH_UNOFFICIAL_BREAKPAD @@ -153,7 +124,7 @@ public: "A serious fault condition occurred in the game. The game will shut down."; std::string message = fmt::format( - "Please send crash.log, crash.dmp, and crash.sav to the developers. " + "Please send crash.json.log, crash.dmp, and crash.sav to the developers. " "This will greatly help debugging.\n\n" "https://github.com/OpenTTD/OpenTTD/issues.\n\n" "{}\n{}\n{}\n{}", diff --git a/src/os/unix/crashlog_unix.cpp b/src/os/unix/crashlog_unix.cpp index 23854d1b0a..dbe4849afb 100644 --- a/src/os/unix/crashlog_unix.cpp +++ b/src/os/unix/crashlog_unix.cpp @@ -49,55 +49,26 @@ class CrashLogUnix : public CrashLog { /** Signal that has been thrown. */ int signum; - void LogOSVersion(std::back_insert_iterator &output_iterator) const override + void SurveyCrash(nlohmann::json &survey) const override { - struct utsname name; - if (uname(&name) < 0) { - fmt::format_to(output_iterator, "Could not get OS version: {}\n", strerror(errno)); - return; - } - - fmt::format_to(output_iterator, - "Operating system:\n" - " Name: {}\n" - " Release: {}\n" - " Version: {}\n" - " Machine: {}\n", - name.sysname, - name.release, - name.version, - name.machine - ); + survey["id"] = signum; + survey["reason"] = strsignal(signum); } - void LogError(std::back_insert_iterator &output_iterator, const std::string_view message) const override + void SurveyStacktrace(nlohmann::json &survey) const override { - fmt::format_to(output_iterator, - "Crash reason:\n" - " Signal: {} ({})\n" - " Message: {}\n\n", - strsignal(this->signum), - this->signum, - message - ); - } - - void LogStacktrace(std::back_insert_iterator &output_iterator) const override - { - fmt::format_to(output_iterator, "Stacktrace:\n"); #if defined(__GLIBC__) void *trace[64]; int trace_size = backtrace(trace, lengthof(trace)); + survey = nlohmann::json::array(); + char **messages = backtrace_symbols(trace, trace_size); for (int i = 0; i < trace_size; i++) { - fmt::format_to(output_iterator, " [{:02}] {}\n", i, messages[i]); + survey.push_back(messages[i]); } free(messages); -#else - fmt::format_to(output_iterator, " Not supported.\n"); #endif - fmt::format_to(output_iterator, "\n"); } #ifdef WITH_UNOFFICIAL_BREAKPAD diff --git a/src/os/windows/crashlog_win.cpp b/src/os/windows/crashlog_win.cpp index 210b6057e6..281c090b50 100644 --- a/src/os/windows/crashlog_win.cpp +++ b/src/os/windows/crashlog_win.cpp @@ -38,6 +38,33 @@ /** Exception code used for custom abort. */ static constexpr DWORD CUSTOM_ABORT_EXCEPTION = 0xE1212012; +/** A map between exception code and its name. */ +static const std::map exception_code_to_name{ + {EXCEPTION_ACCESS_VIOLATION, "EXCEPTION_ACCESS_VIOLATION"}, + {EXCEPTION_ARRAY_BOUNDS_EXCEEDED, "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"}, + {EXCEPTION_BREAKPOINT, "EXCEPTION_BREAKPOINT"}, + {EXCEPTION_DATATYPE_MISALIGNMENT, "EXCEPTION_DATATYPE_MISALIGNMENT"}, + {EXCEPTION_FLT_DENORMAL_OPERAND, "EXCEPTION_FLT_DENORMAL_OPERAND"}, + {EXCEPTION_FLT_DIVIDE_BY_ZERO, "EXCEPTION_FLT_DIVIDE_BY_ZERO"}, + {EXCEPTION_FLT_INEXACT_RESULT, "EXCEPTION_FLT_INEXACT_RESULT"}, + {EXCEPTION_FLT_INVALID_OPERATION, "EXCEPTION_FLT_INVALID_OPERATION"}, + {EXCEPTION_FLT_OVERFLOW, "EXCEPTION_FLT_OVERFLOW"}, + {EXCEPTION_FLT_STACK_CHECK, "EXCEPTION_FLT_STACK_CHECK"}, + {EXCEPTION_FLT_UNDERFLOW, "EXCEPTION_FLT_UNDERFLOW"}, + {EXCEPTION_GUARD_PAGE, "EXCEPTION_GUARD_PAGE"}, + {EXCEPTION_ILLEGAL_INSTRUCTION, "EXCEPTION_ILLEGAL_INSTRUCTION"}, + {EXCEPTION_IN_PAGE_ERROR, "EXCEPTION_IN_PAGE_ERROR"}, + {EXCEPTION_INT_DIVIDE_BY_ZERO, "EXCEPTION_INT_DIVIDE_BY_ZERO"}, + {EXCEPTION_INT_OVERFLOW, "EXCEPTION_INT_OVERFLOW"}, + {EXCEPTION_INVALID_DISPOSITION, "EXCEPTION_INVALID_DISPOSITION"}, + {EXCEPTION_INVALID_HANDLE, "EXCEPTION_INVALID_HANDLE"}, + {EXCEPTION_NONCONTINUABLE_EXCEPTION, "EXCEPTION_NONCONTINUABLE_EXCEPTION"}, + {EXCEPTION_PRIV_INSTRUCTION, "EXCEPTION_PRIV_INSTRUCTION"}, + {EXCEPTION_SINGLE_STEP, "EXCEPTION_SINGLE_STEP"}, + {EXCEPTION_STACK_OVERFLOW, "EXCEPTION_STACK_OVERFLOW"}, + {STATUS_UNWIND_CONSOLIDATE, "STATUS_UNWIND_CONSOLIDATE"}, +}; + /** * Forcefully try to terminate the application. * @@ -57,9 +84,17 @@ class CrashLogWindows : public CrashLog { /** Information about the encountered exception */ EXCEPTION_POINTERS *ep; - void LogOSVersion(std::back_insert_iterator &output_iterator) const override; - void LogError(std::back_insert_iterator &output_iterator, const std::string_view message) const override; - void LogStacktrace(std::back_insert_iterator &output_iterator) const override; + void SurveyCrash(nlohmann::json &survey) const override + { + survey["id"] = ep->ExceptionRecord->ExceptionCode; + if (exception_code_to_name.count(ep->ExceptionRecord->ExceptionCode) > 0) { + survey["reason"] = exception_code_to_name.at(ep->ExceptionRecord->ExceptionCode); + } else { + survey["reason"] = "Unknown exception code"; + } + } + + void SurveyStacktrace(nlohmann::json &survey) const override; public: #ifdef WITH_UNOFFICIAL_BREAKPAD @@ -136,41 +171,11 @@ public: /* static */ CrashLogWindows *CrashLogWindows::current = nullptr; -/* virtual */ void CrashLogWindows::LogOSVersion(std::back_insert_iterator &output_iterator) const -{ - _OSVERSIONINFOA os; - os.dwOSVersionInfoSize = sizeof(os); - GetVersionExA(&os); - - fmt::format_to(output_iterator, - "Operating system:\n" - " Name: Windows\n" - " Release: {}.{}.{} ({})\n", - os.dwMajorVersion, - os.dwMinorVersion, - os.dwBuildNumber, - os.szCSDVersion - ); -} - -/* virtual */ void CrashLogWindows::LogError(std::back_insert_iterator &output_iterator, const std::string_view message) const -{ - fmt::format_to(output_iterator, - "Crash reason:\n" - " Exception: {:08X}\n" - " Location: {:X}\n" - " Message: {}\n\n", - ep->ExceptionRecord->ExceptionCode, - (size_t)ep->ExceptionRecord->ExceptionAddress, - message - ); -} - #if defined(_MSC_VER) static const uint MAX_SYMBOL_LEN = 512; static const uint MAX_FRAMES = 64; -/* virtual */ void CrashLogWindows::LogStacktrace(std::back_insert_iterator &output_iterator) const +/* virtual */ void CrashLogWindows::SurveyStacktrace(nlohmann::json &survey) const { DllLoader dbghelp(L"dbghelp.dll"); struct ProcPtrs { @@ -195,7 +200,7 @@ static const uint MAX_FRAMES = 64; dbghelp.GetProcAddress("SymGetLineFromAddr64"), }; - fmt::format_to(output_iterator, "Stack trace:\n"); + survey = nlohmann::json::array(); /* Try to load the functions from the DLL, if that fails because of a too old dbghelp.dll, just skip it. */ if (dbghelp.Success()) { @@ -246,7 +251,7 @@ static const uint MAX_FRAMES = 64; hCur, GetCurrentThread(), &frame, &ctx, nullptr, proc.pSymFunctionTableAccess64, proc.pSymGetModuleBase64, nullptr)) break; if (frame.AddrPC.Offset == frame.AddrReturn.Offset) { - fmt::format_to(output_iterator, " \n"); + survey.push_back(""); break; } @@ -260,33 +265,31 @@ static const uint MAX_FRAMES = 64; } /* Print module and instruction pointer. */ - fmt::format_to(output_iterator, "[{:02}] {:20s} {:X}", num, mod_name, frame.AddrPC.Offset); + std::string message = fmt::format("{:20s} {:X}", mod_name, frame.AddrPC.Offset); /* Get symbol name and line info if possible. */ DWORD64 offset; if (proc.pSymGetSymFromAddr64(hCur, frame.AddrPC.Offset, &offset, sym_info)) { - fmt::format_to(output_iterator, " {} + {}", sym_info->Name, offset); + message += fmt::format(" {} + {}", sym_info->Name, offset); DWORD line_offs; IMAGEHLP_LINE64 line; line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); if (proc.pSymGetLineFromAddr64(hCur, frame.AddrPC.Offset, &line_offs, &line)) { - fmt::format_to(output_iterator, " ({}:{})", line.FileName, line.LineNumber); + message += fmt::format(" ({}:{})", line.FileName, line.LineNumber); } } - fmt::format_to(output_iterator, "\n"); + + survey.push_back(message); } proc.pSymCleanup(hCur); } - - fmt::format_to(output_iterator, "\n"); } #else -/* virtual */ void CrashLogWindows::LogStacktrace(std::back_insert_iterator &output_iterator) const +/* virtual */ void CrashLogWindows::SurveyStacktrace(nlohmann::json &survey) const { - fmt::format_to(output_iterator, "Stack trace:\n"); - fmt::format_to(output_iterator, " Not supported.\n"); + /* Not supported. */ } #endif /* _MSC_VER */ @@ -430,7 +433,7 @@ static bool _expanded; static const wchar_t _crash_desc[] = L"A serious fault condition occurred in the game. The game will shut down.\n" - L"Please send crash.log, crash.dmp, and crash.sav to the developers.\n" + L"Please send crash.json.log, crash.dmp, and crash.sav to the developers.\n" L"This will greatly help debugging.\n\n" L"https://github.com/OpenTTD/OpenTTD/issues\n\n" L"%s\n%s\n%s\n%s\n"; @@ -462,9 +465,10 @@ static INT_PTR CALLBACK CrashDialogFunc(HWND wnd, UINT msg, WPARAM wParam, LPARA { switch (msg) { case WM_INITDIALOG: { - size_t crashlog_length = CrashLogWindows::current->crashlog.size() + 1; + std::string crashlog = CrashLogWindows::current->survey.dump(4); + size_t crashlog_length = crashlog.size() + 1; /* Reserve extra space for LF to CRLF conversion. */ - crashlog_length += std::count(CrashLogWindows::current->crashlog.begin(), CrashLogWindows::current->crashlog.end(), '\n'); + crashlog_length += std::count(crashlog.begin(), crashlog.end(), '\n'); const size_t filename_count = 4; const size_t filename_buf_length = MAX_PATH + 1; @@ -486,7 +490,7 @@ static INT_PTR CALLBACK CrashDialogFunc(HWND wnd, UINT msg, WPARAM wParam, LPARA char *crashlog_dos_nl = reinterpret_cast(filename_buf + filename_buf_length * filename_count); /* Convert unix -> dos newlines because the edit box only supports that properly. */ - const char *crashlog_unix_nl = CrashLogWindows::current->crashlog.data(); + const char *crashlog_unix_nl = crashlog.data(); char *p = crashlog_dos_nl; char32_t c; while ((c = Utf8Consume(&crashlog_unix_nl))) { diff --git a/src/survey.cpp b/src/survey.cpp index 6d2a6ac312..51b1830450 100644 --- a/src/survey.cpp +++ b/src/survey.cpp @@ -32,6 +32,43 @@ #include "base_media_base.h" #include "blitter/factory.hpp" +#ifdef WITH_ALLEGRO +# include +#endif /* WITH_ALLEGRO */ +#ifdef WITH_FONTCONFIG +# include +#endif /* WITH_FONTCONFIG */ +#ifdef WITH_PNG + /* pngconf.h, included by png.h doesn't like something in the + * freetype headers. As such it's not alphabetically sorted. */ +# include +#endif /* WITH_PNG */ +#ifdef WITH_FREETYPE +# include +# include FT_FREETYPE_H +#endif /* WITH_FREETYPE */ +#ifdef WITH_HARFBUZZ +# include +#endif /* WITH_HARFBUZZ */ +#ifdef WITH_ICU_I18N +# include +#endif /* WITH_ICU_I18N */ +#ifdef WITH_LIBLZMA +# include +#endif +#ifdef WITH_LZO +#include +#endif +#if defined(WITH_SDL) || defined(WITH_SDL2) +# include +#endif /* WITH_SDL || WITH_SDL2 */ +#ifdef WITH_ZLIB +# include +#endif +#ifdef WITH_CURL +# include +#endif + #include "safeguards.h" NLOHMANN_JSON_SERIALIZE_ENUM(GRFStatus, { @@ -116,6 +153,34 @@ void SurveySettings(nlohmann::json &survey) SurveySettingsTable(survey, _company_settings, &_settings_client.company); } +/** + * Convert compiler information to JSON. + * + * @param survey The JSON object. + */ +void SurveyCompiler(nlohmann::json &survey) +{ +#if defined(_MSC_VER) + survey["name"] = "MSVC"; + survey["version"] = _MSC_VER; +#elif defined(__ICC) && defined(__GNUC__) + survey["name"] = "ICC"; + survey["version"] = __ICC; +# if defined(__GNUC__) + survey["extra"] = fmt::format("GCC {}.{}.{} mode", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); +# endif +#elif defined(__GNUC__) + survey["name"] = "GCC"; + survey["version"] = fmt::format("{}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); +#else + survey["name"] = "unknown"; +#endif + +#if defined(__VERSION__) + survey["extra"] = __VERSION__; +#endif +} + /** * Convert generic OpenTTD information to JSON. * @@ -293,6 +358,76 @@ void SurveyGameScript(nlohmann::json &survey) survey = fmt::format("{}.{}", Game::GetInfo()->GetName(), Game::GetInfo()->GetVersion()); } +/** + * Convert compiled libraries information to JSON. + * + * @param survey The JSON object. + */ +void SurveyLibraries(nlohmann::json &survey) +{ +#ifdef WITH_ALLEGRO + survey["allegro"] = std::string(allegro_id); +#endif /* WITH_ALLEGRO */ + +#ifdef WITH_FONTCONFIG + int version = FcGetVersion(); + survey["fontconfig"] = fmt::format("{}.{}.{}", version / 10000, (version / 100) % 100, version % 100); +#endif /* WITH_FONTCONFIG */ + +#ifdef WITH_FREETYPE + FT_Library library; + int major, minor, patch; + FT_Init_FreeType(&library); + FT_Library_Version(library, &major, &minor, &patch); + FT_Done_FreeType(library); + survey["freetype"] = fmt::format("{}.{}.{}", major, minor, patch); +#endif /* WITH_FREETYPE */ + +#if defined(WITH_HARFBUZZ) + survey["harfbuzz"] = hb_version_string(); +#endif /* WITH_HARFBUZZ */ + +#if defined(WITH_ICU_I18N) + /* 4 times 0-255, separated by dots (.) and a trailing '\0' */ + char buf[4 * 3 + 3 + 1]; + UVersionInfo ver; + u_getVersion(ver); + u_versionToString(ver, buf); + survey["icu_i18n"] = buf; +#endif /* WITH_ICU_I18N */ + +#ifdef WITH_LIBLZMA + survey["lzma"] = lzma_version_string(); +#endif + +#ifdef WITH_LZO + survey["lzo"] = lzo_version_string(); +#endif + +#ifdef WITH_PNG + survey["png"] = png_get_libpng_ver(nullptr); +#endif /* WITH_PNG */ + +#ifdef WITH_SDL + const SDL_version *sdl_v = SDL_Linked_Version(); + survey["sdl"] = fmt::format("{}.{}.{}", sdl_v->major, sdl_v->minor, sdl_v->patch); +#elif defined(WITH_SDL2) + SDL_version sdl2_v; + SDL_GetVersion(&sdl2_v); + survey["sdl2"] = fmt::format("{}.{}.{}", sdl2_v.major, sdl2_v.minor, sdl2_v.patch); +#endif + +#ifdef WITH_ZLIB + survey["zlib"] = zlibVersion(); +#endif + +#ifdef WITH_CURL + auto *curl_v = curl_version_info(CURLVERSION_NOW); + survey["curl"] = curl_v->version; + survey["curl_ssl"] = curl_v->ssl_version == nullptr ? "none" : curl_v->ssl_version; +#endif +} + /** * Change the bytes of memory into a textual version rounded up to the biggest unit. * diff --git a/src/survey.h b/src/survey.h index d134b94482..b64c062657 100644 --- a/src/survey.h +++ b/src/survey.h @@ -15,10 +15,12 @@ std::string SurveyMemoryToText(uint64_t memory); void SurveyCompanies(nlohmann::json &survey); +void SurveyCompiler(nlohmann::json &survey); void SurveyConfiguration(nlohmann::json &survey); void SurveyFont(nlohmann::json &survey); void SurveyGameScript(nlohmann::json &survey); void SurveyGrfs(nlohmann::json &survey); +void SurveyLibraries(nlohmann::json &survey); void SurveyOpenTTD(nlohmann::json &survey); void SurveySettings(nlohmann::json &survey); void SurveyTimers(nlohmann::json &survey);