2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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 <http://www.gnu.org/licenses/>.
8 /** @file crashlog.cpp Implementation of generic function to be called to log a crash */
15 #include "music/music_driver.hpp"
16 #include "sound/sound_driver.hpp"
17 #include "video/video_driver.hpp"
18 #include "saveload/saveload.h"
19 #include "screenshot.h"
20 #include "network/network_survey.h"
22 #include "fileio_func.h"
23 #include "fileio_type.h"
25 #include "company_func.h"
26 #include "3rdparty/fmt/chrono.h"
27 #include "3rdparty/fmt/std.h"
28 #include "core/format.hpp"
30 #include "safeguards.h"
32 /* static */ std::string
CrashLog::message
{};
34 /** The version of the schema of the JSON information. */
35 constexpr uint8_t CRASHLOG_SURVEY_VERSION
= 1;
38 * Writes the gamelog data to the buffer.
39 * @param output_iterator Iterator to write the output to.
41 static void SurveyGamelog(nlohmann::json
&json
)
43 json
= nlohmann::json::array();
45 _gamelog
.Print([&json
](const std::string
&s
) {
51 * Writes up to 32 recent news messages to the buffer, with the most recent first.
52 * @param output_iterator Iterator to write the output to.
54 static void SurveyRecentNews(nlohmann::json
&json
)
56 json
= nlohmann::json::array();
59 for (NewsItem
*news
= _latest_news
; i
< 32 && news
!= nullptr; news
= news
->prev
, i
++) {
60 TimerGameCalendar::YearMonthDay ymd
= TimerGameCalendar::ConvertDateToYMD(news
->date
);
61 json
.push_back(fmt::format("({}-{:02}-{:02}) StringID: {}, Type: {}, Ref1: {}, {}, Ref2: {}, {}",
62 ymd
.year
, ymd
.month
+ 1, ymd
.day
, news
->string_id
, news
->type
,
63 news
->reftype1
, news
->ref1
, news
->reftype2
, news
->ref2
));
68 * Create a timestamped filename.
69 * @param ext The extension for the filename.
70 * @param with_dir Whether to prepend the filename with the personal directory.
71 * @return The filename
73 std::string
CrashLog::CreateFileName(const char *ext
, bool with_dir
) const
75 static std::string crashname
;
77 if (crashname
.empty()) {
78 crashname
= fmt::format("crash{:%Y%m%d%H%M%S}", fmt::gmtime(time(nullptr)));
80 return fmt::format("{}{}{}", with_dir
? _personal_dir
: std::string
{}, crashname
, ext
);
84 * Fill the crash log buffer with all data of a crash log.
86 void CrashLog::FillCrashLog()
88 /* Reminder: this JSON is read in an automated fashion.
89 * If any structural changes are applied, please bump the version. */
90 this->survey
["schema"] = CRASHLOG_SURVEY_VERSION
;
91 this->survey
["date"] = fmt::format("{:%Y-%m-%d %H:%M:%S} (UTC)", fmt::gmtime(time(nullptr)));
93 /* If no internal reason was logged, it must be a crash. */
94 if (CrashLog::message
.empty()) {
95 this->SurveyCrash(this->survey
["crash"]);
97 this->survey
["crash"]["reason"] = CrashLog::message
;
98 CrashLog::message
.clear();
101 if (!this->TryExecute("stacktrace", [this]() { this->SurveyStacktrace(this->survey
["stacktrace"]); return true; })) {
102 this->survey
["stacktrace"] = "crashed while gathering information";
106 auto &info
= this->survey
["info"];
107 if (!this->TryExecute("os", [&info
]() { SurveyOS(info
["os"]); return true; })) {
108 info
["os"] = "crashed while gathering information";
110 if (!this->TryExecute("openttd", [&info
]() { SurveyOpenTTD(info
["openttd"]); return true; })) {
111 info
["openttd"] = "crashed while gathering information";
113 if (!this->TryExecute("configuration", [&info
]() { SurveyConfiguration(info
["configuration"]); return true; })) {
114 info
["configuration"] = "crashed while gathering information";
116 if (!this->TryExecute("font", [&info
]() { SurveyFont(info
["font"]); return true; })) {
117 info
["font"] = "crashed while gathering information";
119 if (!this->TryExecute("compiler", [&info
]() { SurveyCompiler(info
["compiler"]); return true; })) {
120 info
["compiler"] = "crashed while gathering information";
122 if (!this->TryExecute("libraries", [&info
]() { SurveyLibraries(info
["libraries"]); return true; })) {
123 info
["libraries"] = "crashed while gathering information";
125 if (!this->TryExecute("plugins", [&info
]() { SurveyPlugins(info
["plugins"]); return true; })) {
126 info
["plugins"] = "crashed while gathering information";
131 auto &game
= this->survey
["game"];
132 game
["local_company"] = _local_company
;
133 game
["current_company"] = _current_company
;
135 if (!this->TryExecute("timers", [&game
]() { SurveyTimers(game
["timers"]); return true; })) {
136 game
["libraries"] = "crashed while gathering information";
138 if (!this->TryExecute("companies", [&game
]() { SurveyCompanies(game
["companies"]); return true; })) {
139 game
["companies"] = "crashed while gathering information";
141 if (!this->TryExecute("settings", [&game
]() { SurveySettings(game
["settings_changed"], true); return true; })) {
142 game
["settings"] = "crashed while gathering information";
144 if (!this->TryExecute("grfs", [&game
]() { SurveyGrfs(game
["grfs"]); return true; })) {
145 game
["grfs"] = "crashed while gathering information";
147 if (!this->TryExecute("game_script", [&game
]() { SurveyGameScript(game
["game_script"]); return true; })) {
148 game
["game_script"] = "crashed while gathering information";
150 if (!this->TryExecute("gamelog", [&game
]() { SurveyGamelog(game
["gamelog"]); return true; })) {
151 game
["gamelog"] = "crashed while gathering information";
153 if (!this->TryExecute("news", [&game
]() { SurveyRecentNews(game
["news"]); return true; })) {
154 game
["news"] = "crashed while gathering information";
159 void CrashLog::PrintCrashLog() const
161 fmt::print(" OpenTTD version:\n");
162 fmt::print(" Version: {}\n", this->survey
["info"]["openttd"]["version"]["revision"].get
<std::string
>());
163 fmt::print(" Hash: {}\n", this->survey
["info"]["openttd"]["version"]["hash"].get
<std::string
>());
164 fmt::print(" NewGRF ver: {}\n", this->survey
["info"]["openttd"]["version"]["newgrf"].get
<std::string
>());
165 fmt::print(" Content ver: {}\n", this->survey
["info"]["openttd"]["version"]["content"].get
<std::string
>());
168 fmt::print(" Crash:\n");
169 fmt::print(" Reason: {}\n", this->survey
["crash"]["reason"].get
<std::string
>());
172 fmt::print(" Stacktrace:\n");
173 for (const auto &line
: this->survey
["stacktrace"]) {
174 fmt::print(" {}\n", line
.get
<std::string
>());
180 * Write the crash log to a file.
181 * @note The filename will be written to \c crashlog_filename.
182 * @return true when the crash log was successfully written.
184 bool CrashLog::WriteCrashLog()
186 this->crashlog_filename
= this->CreateFileName(".json.log");
188 FILE *file
= FioFOpenFile(this->crashlog_filename
, "w", NO_DIRECTORY
);
189 if (file
== nullptr) return false;
191 std::string survey_json
= this->survey
.dump(4);
193 size_t len
= survey_json
.size();
194 size_t written
= fwrite(survey_json
.data(), 1, len
, file
);
197 return len
== written
;
201 * Write the (crash) dump to a file.
203 * @note Sets \c crashdump_filename when there is a successful return.
204 * @return True iff the crashdump was successfully created.
206 /* virtual */ bool CrashLog::WriteCrashDump()
208 fmt::print("No method to create a crash.dmp available.\n");
213 * Write the (crash) savegame to a file.
214 * @note The filename will be written to \c savegame_filename.
215 * @return true when the crash save was successfully made.
217 bool CrashLog::WriteSavegame()
219 /* If the map doesn't exist, saving will fail too. If the map got
220 * initialised, there is a big chance the rest is initialised too. */
221 if (!Map::IsInitialized()) return false;
224 _gamelog
.Emergency();
226 this->savegame_filename
= this->CreateFileName(".sav");
228 /* Don't do a threaded saveload. */
229 return SaveOrLoad(this->savegame_filename
, SLO_SAVE
, DFT_GAME_FILE
, NO_DIRECTORY
, false) == SL_OK
;
236 * Write the (crash) screenshot to a file.
237 * @note The filename will be written to \c screenshot_filename.
238 * @return std::nullopt when the crash screenshot could not be made, otherwise the filename.
240 bool CrashLog::WriteScreenshot()
242 /* Don't draw when we have invalid screen size */
243 if (_screen
.width
< 1 || _screen
.height
< 1 || _screen
.dst_ptr
== nullptr) return false;
245 std::string filename
= this->CreateFileName("", false);
246 bool res
= MakeScreenshot(SC_CRASHLOG
, filename
);
247 if (res
) this->screenshot_filename
= _full_screenshot_path
;
252 * Send the survey result, noting it was a crash.
254 void CrashLog::SendSurvey() const
256 if (_game_mode
== GM_NORMAL
) {
257 _survey
.Transmit(NetworkSurveyHandler::Reason::CRASH
, true);
262 * Makes the crash log, writes it to a file and then subsequently tries
263 * to make a crash dump and crash savegame. It uses DEBUG to write
264 * information like paths to the console.
266 void CrashLog::MakeCrashLog()
268 /* Don't keep looping logging crashes. */
269 static bool crashlogged
= false;
270 if (crashlogged
) return;
273 fmt::print("Crash encountered, generating crash log...\n");
274 this->FillCrashLog();
275 fmt::print("Crash log generated.\n\n");
277 fmt::print("Crash in summary:\n");
278 this->TryExecute("crashlog", [this]() { this->PrintCrashLog(); return true; });
280 fmt::print("Writing crash log to disk...\n");
281 bool ret
= this->TryExecute("crashlog", [this]() { return this->WriteCrashLog(); });
283 fmt::print("Crash log written to {}. Please add this file to any bug reports.\n\n", this->crashlog_filename
);
285 fmt::print("Writing crash log failed. Please attach the output above to any bug reports.\n\n");
286 this->crashlog_filename
= "(failed to write crash log)";
289 fmt::print("Writing crash dump to disk...\n");
290 ret
= this->TryExecute("crashdump", [this]() { return this->WriteCrashDump(); });
292 fmt::print("Crash dump written to {}. Please add this file to any bug reports.\n\n", this->crashdump_filename
);
294 fmt::print("Writing crash dump failed.\n\n");
295 this->crashdump_filename
= "(failed to write crash dump)";
298 fmt::print("Writing crash savegame...\n");
299 ret
= this->TryExecute("savegame", [this]() { return this->WriteSavegame(); });
301 fmt::print("Crash savegame written to {}. Please add this file and the last (auto)save to any bug reports.\n\n", this->savegame_filename
);
303 fmt::print("Writing crash savegame failed. Please attach the last (auto)save to any bug reports.\n\n");
304 this->savegame_filename
= "(failed to write crash savegame)";
307 fmt::print("Writing crash screenshot...\n");
308 ret
= this->TryExecute("screenshot", [this]() { return this->WriteScreenshot(); });
310 fmt::print("Crash screenshot written to {}. Please add this file to any bug reports.\n\n", this->screenshot_filename
);
312 fmt::print("Writing crash screenshot failed.\n\n");
313 this->screenshot_filename
= "(failed to write crash screenshot)";
316 this->TryExecute("survey", [this]() { this->SendSurvey(); return true; });
320 * Sets a message for the error message handler.
321 * @param message The error message of the error.
323 /* static */ void CrashLog::SetErrorMessage(const std::string
&message
)
325 CrashLog::message
= message
;
329 * Try to close the sound/video stuff so it doesn't keep lingering around
330 * incorrect video states or so, e.g. keeping dpmi disabled.
332 /* static */ void CrashLog::AfterCrashLogCleanup()
334 if (MusicDriver::GetInstance() != nullptr) MusicDriver::GetInstance()->Stop();
335 if (SoundDriver::GetInstance() != nullptr) SoundDriver::GetInstance()->Stop();
336 if (VideoDriver::GetInstance() != nullptr) VideoDriver::GetInstance()->Stop();