From 37e2f99c092827686722895dc8d758787132610d Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Thu, 14 Sep 2023 20:13:27 +0200 Subject: [PATCH] Change: store crash logs in JSON format (#11232) --- src/CMakeLists.txt | 2 + src/crashlog.cpp | 392 ++++++++------------------- src/crashlog.h | 48 ++-- src/network/network_survey.cpp | 316 +--------------------- src/os/macosx/crashlog_osx.cpp | 45 +--- src/os/macosx/survey_osx.cpp | 4 +- src/os/unix/crashlog_unix.cpp | 43 +-- src/os/unix/survey_unix.cpp | 5 +- src/os/windows/crashlog_win.cpp | 102 +++---- src/os/windows/survey_win.cpp | 4 +- src/settings.cpp | 18 ++ src/settings_internal.h | 12 + src/survey.cpp | 458 ++++++++++++++++++++++++++++++++ src/survey.h | 31 +++ 14 files changed, 729 insertions(+), 751 deletions(-) create mode 100644 src/survey.cpp create mode 100644 src/survey.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f62231b3c9..8c084c913f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -432,6 +432,8 @@ add_files( subsidy_func.h subsidy_gui.cpp subsidy_type.h + survey.cpp + survey.h tar_type.h terraform_cmd.cpp terraform_cmd.h diff --git a/src/crashlog.cpp b/src/crashlog.cpp index 1ea5b877ac..4be2605652 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_changed"], true); 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 297d4e6e05..96f9c964a1 100644 --- a/src/network/network_survey.cpp +++ b/src/network/network_survey.cpp @@ -12,26 +12,9 @@ #include "settings_table.h" #include "network.h" #include "../debug.h" -#include "../rev.h" -#include "../settings_type.h" -#include "../timer/timer_game_tick.h" - -#include "../currency.h" -#include "../fontcache.h" -#include "../language.h" - -#include "../ai/ai_info.hpp" -#include "../game/game.hpp" -#include "../game/game_info.hpp" - -#include "../music/music_driver.hpp" -#include "../sound/sound_driver.hpp" -#include "../video/video_driver.hpp" - -#include "../base_media_base.h" -#include "../blitter/factory.hpp" - -#include "../3rdparty/nlohmann/json.hpp" +#include "../survey.h" +#include "../3rdparty/fmt/chrono.h" +#include "../3rdparty/fmt/std.h" #include "../safeguards.h" @@ -46,294 +29,6 @@ NLOHMANN_JSON_SERIALIZE_ENUM(NetworkSurveyHandler::Reason, { {NetworkSurveyHandler::Reason::CRASH, "crash"}, }) -NLOHMANN_JSON_SERIALIZE_ENUM(GRFStatus, { - {GRFStatus::GCS_UNKNOWN, "unknown"}, - {GRFStatus::GCS_DISABLED, "disabled"}, - {GRFStatus::GCS_NOT_FOUND, "not found"}, - {GRFStatus::GCS_INITIALISED, "initialised"}, - {GRFStatus::GCS_ACTIVATED, "activated"}, -}) - -static const std::string _vehicle_type_to_string[] = { - "train", - "roadveh", - "ship", - "aircraft", -}; - -/* Defined in one of the os/ survey files. */ -extern void SurveyOS(nlohmann::json &json); - -/** - * List of all the generic setting tables. - * - * There are a few tables that are special and not processed like the rest: - * - _currency_settings - * - _misc_settings - * - _company_settings - * - _win32_settings - * As such, they are not part of this list. - */ -static auto &GenericSettingTables() -{ - static const SettingTable _generic_setting_tables[] = { - _difficulty_settings, - _economy_settings, - _game_settings, - _gui_settings, - _linkgraph_settings, - _locale_settings, - _multimedia_settings, - _network_settings, - _news_display_settings, - _pathfinding_settings, - _script_settings, - _world_settings, - }; - return _generic_setting_tables; -} - -/** - * Convert a settings table to JSON. - * - * @param survey The JSON object. - * @param table The settings table to convert. - * @param object The object to get the settings from. - */ -static void SurveySettingsTable(nlohmann::json &survey, const SettingTable &table, void *object) -{ - for (auto &desc : table) { - const SettingDesc *sd = GetSettingDesc(desc); - /* Skip any old settings we no longer save/load. */ - if (!SlIsObjectCurrentlyValid(sd->save.version_from, sd->save.version_to)) continue; - - auto name = sd->GetName(); - survey[name] = sd->FormatValue(object); - } -} - -/** - * Convert settings to JSON. - * - * @param survey The JSON object. - */ -static void SurveySettings(nlohmann::json &survey) -{ - SurveySettingsTable(survey, _misc_settings, nullptr); -#if defined(_WIN32) && !defined(DEDICATED) - SurveySettingsTable(survey, _win32_settings, nullptr); -#endif - for (auto &table : GenericSettingTables()) { - SurveySettingsTable(survey, table, &_settings_game); - } - SurveySettingsTable(survey, _currency_settings, &_custom_currency); - SurveySettingsTable(survey, _company_settings, &_settings_client.company); -} - -/** - * Convert generic OpenTTD information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyOpenTTD(nlohmann::json &survey) -{ - survey["version"]["revision"] = std::string(_openttd_revision); - survey["version"]["modified"] = _openttd_revision_modified; - survey["version"]["tagged"] = _openttd_revision_tagged; - survey["version"]["hash"] = std::string(_openttd_revision_hash); - survey["version"]["newgrf"] = fmt::format("{:X}", _openttd_newgrf_version); - survey["version"]["content"] = std::string(_openttd_content_version); - survey["build_date"] = std::string(_openttd_build_date); - survey["bits"] = -#ifdef POINTER_IS_64BIT - 64 -#else - 32 -#endif - ; - survey["endian"] = -#if (TTD_ENDIAN == TTD_LITTLE_ENDIAN) - "little" -#else - "big" -#endif - ; - survey["dedicated_build"] = -#ifdef DEDICATED - "yes" -#else - "no" -#endif - ; -} - -/** - * Convert generic game information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyConfiguration(nlohmann::json &survey) -{ - survey["network"] = _networking ? (_network_server ? "server" : "client") : "no"; - if (_current_language != nullptr) { - survey["language"]["filename"] = _current_language->file.filename().string(); - survey["language"]["name"] = _current_language->name; - survey["language"]["isocode"] = _current_language->isocode; - } - if (BlitterFactory::GetCurrentBlitter() != nullptr) { - survey["blitter"] = BlitterFactory::GetCurrentBlitter()->GetName(); - } - if (MusicDriver::GetInstance() != nullptr) { - survey["music_driver"] = MusicDriver::GetInstance()->GetName(); - } - if (SoundDriver::GetInstance() != nullptr) { - survey["sound_driver"] = SoundDriver::GetInstance()->GetName(); - } - if (VideoDriver::GetInstance() != nullptr) { - survey["video_driver"] = VideoDriver::GetInstance()->GetName(); - survey["video_info"] = VideoDriver::GetInstance()->GetInfoString(); - } - if (BaseGraphics::GetUsedSet() != nullptr) { - survey["graphics_set"] = fmt::format("{}.{}", BaseGraphics::GetUsedSet()->name, BaseGraphics::GetUsedSet()->version); - } - if (BaseMusic::GetUsedSet() != nullptr) { - survey["music_set"] = fmt::format("{}.{}", BaseMusic::GetUsedSet()->name, BaseMusic::GetUsedSet()->version); - } - if (BaseSounds::GetUsedSet() != nullptr) { - survey["sound_set"] = fmt::format("{}.{}", BaseSounds::GetUsedSet()->name, BaseSounds::GetUsedSet()->version); - } -} - -/** - * Convert font information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyFont(nlohmann::json &survey) -{ - survey["small"] = FontCache::Get(FS_SMALL)->GetFontName(); - survey["medium"] = FontCache::Get(FS_NORMAL)->GetFontName(); - survey["large"] = FontCache::Get(FS_LARGE)->GetFontName(); - survey["mono"] = FontCache::Get(FS_MONO)->GetFontName(); -} - -/** - * Convert company information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyCompanies(nlohmann::json &survey) -{ - for (const Company *c : Company::Iterate()) { - auto &company = survey[std::to_string(c->index)]; - if (c->ai_info == nullptr) { - company["type"] = "human"; - } else { - company["type"] = "ai"; - company["script"] = fmt::format("{}.{}", c->ai_info->GetName(), c->ai_info->GetVersion()); - } - - for (VehicleType type = VEH_BEGIN; type < VEH_COMPANY_END; type++) { - uint amount = c->group_all[type].num_vehicle; - company["vehicles"][_vehicle_type_to_string[type]] = amount; - } - - company["infrastructure"]["road"] = c->infrastructure.GetRoadTotal(); - company["infrastructure"]["tram"] = c->infrastructure.GetTramTotal(); - company["infrastructure"]["rail"] = c->infrastructure.GetRailTotal(); - company["infrastructure"]["signal"] = c->infrastructure.signal; - company["infrastructure"]["water"] = c->infrastructure.water; - company["infrastructure"]["station"] = c->infrastructure.station; - company["infrastructure"]["airport"] = c->infrastructure.airport; - } -} - -/** - * Convert timer information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyTimers(nlohmann::json &survey) -{ - survey["ticks"] = TimerGameTick::counter; - survey["seconds"] = std::chrono::duration_cast(std::chrono::steady_clock::now() - _switch_mode_time).count(); - - TimerGameCalendar::YearMonthDay ymd; - TimerGameCalendar::ConvertDateToYMD(TimerGameCalendar::date, &ymd); - survey["calendar"] = fmt::format("{:04}-{:02}-{:02} ({})", ymd.year, ymd.month + 1, ymd.day, TimerGameCalendar::date_fract); -} - -/** - * Convert GRF information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyGrfs(nlohmann::json &survey) -{ - for (GRFConfig *c = _grfconfig; c != nullptr; c = c->next) { - auto grfid = fmt::format("{:08x}", BSWAP32(c->ident.grfid)); - auto &grf = survey[grfid]; - - grf["md5sum"] = FormatArrayAsHex(c->ident.md5sum); - grf["status"] = c->status; - - if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_UNSET) grf["palette"] = "unset"; - if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_DOS) grf["palette"] = "dos"; - if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_WINDOWS) grf["palette"] = "windows"; - if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_ANY) grf["palette"] = "any"; - - if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_UNSET) grf["blitter"] = "unset"; - if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_32BPP) grf["blitter"] = "32bpp"; - - grf["is_static"] = HasBit(c->flags, GCF_STATIC); - - std::vector parameters; - for (int i = 0; i < c->num_params; i++) { - parameters.push_back(c->param[i]); - } - grf["parameters"] = parameters; - } -} - -/** - * Convert game-script information to JSON. - * - * @param survey The JSON object. - */ -static void SurveyGameScript(nlohmann::json &survey) -{ - if (Game::GetInfo() == nullptr) return; - - survey = fmt::format("{}.{}", Game::GetInfo()->GetName(), Game::GetInfo()->GetVersion()); -} - -/** - * Change the bytes of memory into a textual version rounded up to the biggest unit. - * - * For example, 16751108096 would become 16 GiB. - * - * @param memory The bytes of memory. - * @return std::string A textual representation. - */ -std::string SurveyMemoryToText(uint64_t memory) -{ - memory = memory / 1024; // KiB - memory = CeilDiv(memory, 1024); // MiB - - /* Anything above 512 MiB we represent in GiB. */ - if (memory > 512) { - return fmt::format("{} GiB", CeilDiv(memory, 1024)); - } - - /* Anything above 64 MiB we represent in a multiplier of 128 MiB. */ - if (memory > 64) { - return fmt::format("{} MiB", Ceil(memory, 128)); - } - - /* Anything else in a multiplier of 4 MiB. */ - return fmt::format("{} MiB", Ceil(memory, 4)); -} - /** * Create the payload for the survey. * @@ -348,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. */ @@ -362,13 +58,15 @@ std::string NetworkSurveyHandler::CreatePayload(Reason reason, bool for_preview) SurveyOpenTTD(info["openttd"]); SurveyConfiguration(info["configuration"]); SurveyFont(info["font"]); + SurveyCompiler(info["compiler"]); + SurveyLibraries(info["libraries"]); } { auto &game = survey["game"]; SurveyTimers(game["timers"]); SurveyCompanies(game["companies"]); - SurveySettings(game["settings"]); + SurveySettings(game["settings"], false); SurveyGrfs(game["grfs"]); SurveyGameScript(game["game_script"]); } 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/macosx/survey_osx.cpp b/src/os/macosx/survey_osx.cpp index 40839ea614..2a3999936b 100644 --- a/src/os/macosx/survey_osx.cpp +++ b/src/os/macosx/survey_osx.cpp @@ -10,7 +10,7 @@ #include "../../stdafx.h" #include "../../3rdparty/fmt/format.h" -#include "../../3rdparty/nlohmann/json.hpp" +#include "../../survey.h" #include "macos.h" #include @@ -18,8 +18,6 @@ #include "../../safeguards.h" -extern std::string SurveyMemoryToText(uint64_t memory); - void SurveyOS(nlohmann::json &json) { int ver_maj, ver_min, ver_bug; 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/unix/survey_unix.cpp b/src/os/unix/survey_unix.cpp index ac1368f9a7..d1f07d116f 100644 --- a/src/os/unix/survey_unix.cpp +++ b/src/os/unix/survey_unix.cpp @@ -8,8 +8,7 @@ /** @file survey_unix.cpp Unix implementation of OS-specific survey information. */ #include "../../stdafx.h" - -#include "../../3rdparty/nlohmann/json.hpp" +#include "../../survey.h" #include #include @@ -17,8 +16,6 @@ #include "../../safeguards.h" -extern std::string SurveyMemoryToText(uint64_t memory); - void SurveyOS(nlohmann::json &json) { struct utsname name; 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/os/windows/survey_win.cpp b/src/os/windows/survey_win.cpp index ace726b1ed..a5d21ea600 100644 --- a/src/os/windows/survey_win.cpp +++ b/src/os/windows/survey_win.cpp @@ -10,15 +10,13 @@ #include "../../stdafx.h" #include "../../3rdparty/fmt/format.h" -#include "../../3rdparty/nlohmann/json.hpp" +#include "../../survey.h" #include #include #include "../../safeguards.h" -extern std::string SurveyMemoryToText(uint64_t memory); - void SurveyOS(nlohmann::json &json) { _OSVERSIONINFOA os; diff --git a/src/settings.cpp b/src/settings.cpp index cfd6dedba0..42c2fe0d2d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -716,6 +716,12 @@ bool IntSettingDesc::IsSameValue(const IniItem *item, void *object) const return item_value == object_value; } +bool IntSettingDesc::IsDefaultValue(void *object) const +{ + int32_t object_value = this->Read(object); + return this->def == object_value; +} + std::string StringSettingDesc::FormatValue(const void *object) const { const std::string &str = this->Read(object); @@ -742,12 +748,24 @@ bool StringSettingDesc::IsSameValue(const IniItem *item, void *object) const return item->value->compare(str) == 0; } +bool StringSettingDesc::IsDefaultValue(void *object) const +{ + const std::string &str = this->Read(object); + return this->def == str; +} + bool ListSettingDesc::IsSameValue(const IniItem *item, void *object) const { /* Checking for equality is way more expensive than just writing the value. */ return false; } +bool ListSettingDesc::IsDefaultValue(void *object) const +{ + /* Defaults of lists are often complicated, and hard to compare. */ + return false; +} + /** * Loads all items from a 'grpname' section into a list * The list parameter can be a nullptr pointer, in this case nothing will be diff --git a/src/settings_internal.h b/src/settings_internal.h index 5bee8ba84b..9c791cb0c5 100644 --- a/src/settings_internal.h +++ b/src/settings_internal.h @@ -130,6 +130,14 @@ struct SettingDesc { * @return True if the value is definitely the same (might be false when the same). */ virtual bool IsSameValue(const IniItem *item, void *object) const = 0; + + /** + * Check whether the value is the same as the default value. + * + * @param object The object the setting is in. + * @return true iff the value is the default value. + */ + virtual bool IsDefaultValue(void *object) const = 0; }; /** Base integer type, including boolean, settings. Only these are shown in the settings UI. */ @@ -215,6 +223,7 @@ struct IntSettingDesc : SettingDesc { std::string FormatValue(const void *object) const override; void ParseValue(const IniItem *item, void *object) const override; bool IsSameValue(const IniItem *item, void *object) const override; + bool IsDefaultValue(void *object) const override; int32_t Read(const void *object) const; private: @@ -307,6 +316,7 @@ struct StringSettingDesc : SettingDesc { std::string FormatValue(const void *object) const override; void ParseValue(const IniItem *item, void *object) const override; bool IsSameValue(const IniItem *item, void *object) const override; + bool IsDefaultValue(void *object) const override; const std::string &Read(const void *object) const; private: @@ -324,6 +334,7 @@ struct ListSettingDesc : SettingDesc { std::string FormatValue(const void *object) const override; void ParseValue(const IniItem *item, void *object) const override; bool IsSameValue(const IniItem *item, void *object) const override; + bool IsDefaultValue(void *object) const override; }; /** Placeholder for settings that have been removed, but might still linger in the savegame. */ @@ -334,6 +345,7 @@ struct NullSettingDesc : SettingDesc { std::string FormatValue(const void *object) const override { NOT_REACHED(); } void ParseValue(const IniItem *item, void *object) const override { NOT_REACHED(); } bool IsSameValue(const IniItem *item, void *object) const override { NOT_REACHED(); } + bool IsDefaultValue(void *object) const override { NOT_REACHED(); } }; typedef std::variant SettingVariant; diff --git a/src/survey.cpp b/src/survey.cpp new file mode 100644 index 0000000000..f9d0ac3ca8 --- /dev/null +++ b/src/survey.cpp @@ -0,0 +1,458 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file survey.cpp Functions to survey the current game / system, for crashlog and network-survey. */ + +#include "stdafx.h" + +#include "survey.h" + +#include "settings_table.h" +#include "network/network.h" +#include "rev.h" +#include "settings_type.h" +#include "timer/timer_game_tick.h" + +#include "currency.h" +#include "fontcache.h" +#include "language.h" + +#include "ai/ai_info.hpp" +#include "game/game.hpp" +#include "game/game_info.hpp" + +#include "music/music_driver.hpp" +#include "sound/sound_driver.hpp" +#include "video/video_driver.hpp" + +#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, { + {GRFStatus::GCS_UNKNOWN, "unknown"}, + {GRFStatus::GCS_DISABLED, "disabled"}, + {GRFStatus::GCS_NOT_FOUND, "not found"}, + {GRFStatus::GCS_INITIALISED, "initialised"}, + {GRFStatus::GCS_ACTIVATED, "activated"}, +}) + +/** Lookup table to convert a VehicleType to a string. */ +static const std::string _vehicle_type_to_string[] = { + "train", + "roadveh", + "ship", + "aircraft", +}; + +/** + * List of all the generic setting tables. + * + * There are a few tables that are special and not processed like the rest: + * - _currency_settings + * - _misc_settings + * - _company_settings + * - _win32_settings + * As such, they are not part of this list. + */ +static auto &GenericSettingTables() +{ + static const SettingTable _generic_setting_tables[] = { + _difficulty_settings, + _economy_settings, + _game_settings, + _gui_settings, + _linkgraph_settings, + _locale_settings, + _multimedia_settings, + _network_settings, + _news_display_settings, + _pathfinding_settings, + _script_settings, + _world_settings, + }; + return _generic_setting_tables; +} + +/** + * Convert a settings table to JSON. + * + * @param survey The JSON object. + * @param table The settings table to convert. + * @param object The object to get the settings from. + * @param skip_if_default If true, skip any settings that are on their default value. + */ +static void SurveySettingsTable(nlohmann::json &survey, const SettingTable &table, void *object, bool skip_if_default) +{ + for (auto &desc : table) { + const SettingDesc *sd = GetSettingDesc(desc); + /* Skip any old settings we no longer save/load. */ + if (!SlIsObjectCurrentlyValid(sd->save.version_from, sd->save.version_to)) continue; + + auto name = sd->GetName(); + if (skip_if_default && sd->IsDefaultValue(object)) continue; + survey[name] = sd->FormatValue(object); + } +} + +/** + * Convert settings to JSON. + * + * @param survey The JSON object. + */ +void SurveySettings(nlohmann::json &survey, bool skip_if_default) +{ + SurveySettingsTable(survey, _misc_settings, nullptr, skip_if_default); +#if defined(_WIN32) && !defined(DEDICATED) + SurveySettingsTable(survey, _win32_settings, nullptr, skip_if_default); +#endif + for (auto &table : GenericSettingTables()) { + SurveySettingsTable(survey, table, &_settings_game, skip_if_default); + } + SurveySettingsTable(survey, _currency_settings, &_custom_currency, skip_if_default); + SurveySettingsTable(survey, _company_settings, &_settings_client.company, skip_if_default); +} + +/** + * 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. + * + * @param survey The JSON object. + */ +void SurveyOpenTTD(nlohmann::json &survey) +{ + survey["version"]["revision"] = std::string(_openttd_revision); + survey["version"]["modified"] = _openttd_revision_modified; + survey["version"]["tagged"] = _openttd_revision_tagged; + survey["version"]["hash"] = std::string(_openttd_revision_hash); + survey["version"]["newgrf"] = fmt::format("{:X}", _openttd_newgrf_version); + survey["version"]["content"] = std::string(_openttd_content_version); + survey["build_date"] = std::string(_openttd_build_date); + survey["bits"] = +#ifdef POINTER_IS_64BIT + 64 +#else + 32 +#endif + ; + survey["endian"] = +#if (TTD_ENDIAN == TTD_LITTLE_ENDIAN) + "little" +#else + "big" +#endif + ; + survey["dedicated_build"] = +#ifdef DEDICATED + "yes" +#else + "no" +#endif + ; +} + +/** + * Convert generic game information to JSON. + * + * @param survey The JSON object. + */ +void SurveyConfiguration(nlohmann::json &survey) +{ + survey["network"] = _networking ? (_network_server ? "server" : "client") : "no"; + if (_current_language != nullptr) { + survey["language"]["filename"] = _current_language->file.filename().string(); + survey["language"]["name"] = _current_language->name; + survey["language"]["isocode"] = _current_language->isocode; + } + if (BlitterFactory::GetCurrentBlitter() != nullptr) { + survey["blitter"] = BlitterFactory::GetCurrentBlitter()->GetName(); + } + if (MusicDriver::GetInstance() != nullptr) { + survey["music_driver"] = MusicDriver::GetInstance()->GetName(); + } + if (SoundDriver::GetInstance() != nullptr) { + survey["sound_driver"] = SoundDriver::GetInstance()->GetName(); + } + if (VideoDriver::GetInstance() != nullptr) { + survey["video_driver"] = VideoDriver::GetInstance()->GetName(); + survey["video_info"] = VideoDriver::GetInstance()->GetInfoString(); + } + if (BaseGraphics::GetUsedSet() != nullptr) { + survey["graphics_set"] = fmt::format("{}.{}", BaseGraphics::GetUsedSet()->name, BaseGraphics::GetUsedSet()->version); + } + if (BaseMusic::GetUsedSet() != nullptr) { + survey["music_set"] = fmt::format("{}.{}", BaseMusic::GetUsedSet()->name, BaseMusic::GetUsedSet()->version); + } + if (BaseSounds::GetUsedSet() != nullptr) { + survey["sound_set"] = fmt::format("{}.{}", BaseSounds::GetUsedSet()->name, BaseSounds::GetUsedSet()->version); + } +} + +/** + * Convert font information to JSON. + * + * @param survey The JSON object. + */ +void SurveyFont(nlohmann::json &survey) +{ + survey["small"] = FontCache::Get(FS_SMALL)->GetFontName(); + survey["medium"] = FontCache::Get(FS_NORMAL)->GetFontName(); + survey["large"] = FontCache::Get(FS_LARGE)->GetFontName(); + survey["mono"] = FontCache::Get(FS_MONO)->GetFontName(); +} + +/** + * Convert company information to JSON. + * + * @param survey The JSON object. + */ +void SurveyCompanies(nlohmann::json &survey) +{ + for (const Company *c : Company::Iterate()) { + auto &company = survey[std::to_string(c->index)]; + if (c->ai_info == nullptr) { + company["type"] = "human"; + } else { + company["type"] = "ai"; + company["script"] = fmt::format("{}.{}", c->ai_info->GetName(), c->ai_info->GetVersion()); + } + + for (VehicleType type = VEH_BEGIN; type < VEH_COMPANY_END; type++) { + uint amount = c->group_all[type].num_vehicle; + company["vehicles"][_vehicle_type_to_string[type]] = amount; + } + + company["infrastructure"]["road"] = c->infrastructure.GetRoadTotal(); + company["infrastructure"]["tram"] = c->infrastructure.GetTramTotal(); + company["infrastructure"]["rail"] = c->infrastructure.GetRailTotal(); + company["infrastructure"]["signal"] = c->infrastructure.signal; + company["infrastructure"]["water"] = c->infrastructure.water; + company["infrastructure"]["station"] = c->infrastructure.station; + company["infrastructure"]["airport"] = c->infrastructure.airport; + } +} + +/** + * Convert timer information to JSON. + * + * @param survey The JSON object. + */ +void SurveyTimers(nlohmann::json &survey) +{ + survey["ticks"] = TimerGameTick::counter; + survey["seconds"] = std::chrono::duration_cast(std::chrono::steady_clock::now() - _switch_mode_time).count(); + + TimerGameCalendar::YearMonthDay ymd; + TimerGameCalendar::ConvertDateToYMD(TimerGameCalendar::date, &ymd); + survey["calendar"] = fmt::format("{:04}-{:02}-{:02} ({})", ymd.year, ymd.month + 1, ymd.day, TimerGameCalendar::date_fract); +} + +/** + * Convert GRF information to JSON. + * + * @param survey The JSON object. + */ +void SurveyGrfs(nlohmann::json &survey) +{ + for (GRFConfig *c = _grfconfig; c != nullptr; c = c->next) { + auto grfid = fmt::format("{:08x}", BSWAP32(c->ident.grfid)); + auto &grf = survey[grfid]; + + grf["md5sum"] = FormatArrayAsHex(c->ident.md5sum); + grf["status"] = c->status; + + if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_UNSET) grf["palette"] = "unset"; + if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_DOS) grf["palette"] = "dos"; + if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_WINDOWS) grf["palette"] = "windows"; + if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_ANY) grf["palette"] = "any"; + + if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_UNSET) grf["blitter"] = "unset"; + if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_32BPP) grf["blitter"] = "32bpp"; + + grf["is_static"] = HasBit(c->flags, GCF_STATIC); + + std::vector parameters; + for (int i = 0; i < c->num_params; i++) { + parameters.push_back(c->param[i]); + } + grf["parameters"] = parameters; + } +} + +/** + * Convert game-script information to JSON. + * + * @param survey The JSON object. + */ +void SurveyGameScript(nlohmann::json &survey) +{ + if (Game::GetInfo() == nullptr) return; + + 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. + * + * For example, 16751108096 would become 16 GiB. + * + * @param memory The bytes of memory. + * @return std::string A textual representation. + */ +std::string SurveyMemoryToText(uint64_t memory) +{ + memory = memory / 1024; // KiB + memory = CeilDiv(memory, 1024); // MiB + + /* Anything above 512 MiB we represent in GiB. */ + if (memory > 512) { + return fmt::format("{} GiB", CeilDiv(memory, 1024)); + } + + /* Anything above 64 MiB we represent in a multiplier of 128 MiB. */ + if (memory > 64) { + return fmt::format("{} MiB", Ceil(memory, 128)); + } + + /* Anything else in a multiplier of 4 MiB. */ + return fmt::format("{} MiB", Ceil(memory, 4)); +} diff --git a/src/survey.h b/src/survey.h new file mode 100644 index 0000000000..aae16d4bf4 --- /dev/null +++ b/src/survey.h @@ -0,0 +1,31 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file survey.h Functions to survey the current game / system, for crashlog and network-survey. */ + +#ifndef SURVEY_H +#define SURVEY_H + +#include "3rdparty/nlohmann/json.hpp" + +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, bool skip_if_default); +void SurveyTimers(nlohmann::json &survey); + +/* Defined in os//survey_.cpp. */ +void SurveyOS(nlohmann::json &json); + +#endif /* SURVEY_H */