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_win.cpp Implementation of a crashlogger for Windows */
10 #include "../../stdafx.h"
11 #include "../../crashlog.h"
13 #include "../../core/math_func.hpp"
14 #include "../../string_func.h"
15 #include "../../fileio_func.h"
16 #include "../../strings_func.h"
17 #include "../../gamelog.h"
18 #include "../../saveload/saveload.h"
19 #include "../../video/video_driver.hpp"
20 #include "../../library_loader.h"
33 #ifdef WITH_UNOFFICIAL_BREAKPAD
34 # include <client/windows/handler/exception_handler.h>
37 #include "../../safeguards.h"
39 /** Exception code used for custom abort. */
40 static constexpr DWORD CUSTOM_ABORT_EXCEPTION
= 0xE1212012;
42 /** A map between exception code and its name. */
43 static const std::map
<DWORD
, std::string
> exception_code_to_name
{
44 {EXCEPTION_ACCESS_VIOLATION
, "EXCEPTION_ACCESS_VIOLATION"},
45 {EXCEPTION_ARRAY_BOUNDS_EXCEEDED
, "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"},
46 {EXCEPTION_BREAKPOINT
, "EXCEPTION_BREAKPOINT"},
47 {EXCEPTION_DATATYPE_MISALIGNMENT
, "EXCEPTION_DATATYPE_MISALIGNMENT"},
48 {EXCEPTION_FLT_DENORMAL_OPERAND
, "EXCEPTION_FLT_DENORMAL_OPERAND"},
49 {EXCEPTION_FLT_DIVIDE_BY_ZERO
, "EXCEPTION_FLT_DIVIDE_BY_ZERO"},
50 {EXCEPTION_FLT_INEXACT_RESULT
, "EXCEPTION_FLT_INEXACT_RESULT"},
51 {EXCEPTION_FLT_INVALID_OPERATION
, "EXCEPTION_FLT_INVALID_OPERATION"},
52 {EXCEPTION_FLT_OVERFLOW
, "EXCEPTION_FLT_OVERFLOW"},
53 {EXCEPTION_FLT_STACK_CHECK
, "EXCEPTION_FLT_STACK_CHECK"},
54 {EXCEPTION_FLT_UNDERFLOW
, "EXCEPTION_FLT_UNDERFLOW"},
55 {EXCEPTION_GUARD_PAGE
, "EXCEPTION_GUARD_PAGE"},
56 {EXCEPTION_ILLEGAL_INSTRUCTION
, "EXCEPTION_ILLEGAL_INSTRUCTION"},
57 {EXCEPTION_IN_PAGE_ERROR
, "EXCEPTION_IN_PAGE_ERROR"},
58 {EXCEPTION_INT_DIVIDE_BY_ZERO
, "EXCEPTION_INT_DIVIDE_BY_ZERO"},
59 {EXCEPTION_INT_OVERFLOW
, "EXCEPTION_INT_OVERFLOW"},
60 {EXCEPTION_INVALID_DISPOSITION
, "EXCEPTION_INVALID_DISPOSITION"},
61 {EXCEPTION_INVALID_HANDLE
, "EXCEPTION_INVALID_HANDLE"},
62 {EXCEPTION_NONCONTINUABLE_EXCEPTION
, "EXCEPTION_NONCONTINUABLE_EXCEPTION"},
63 {EXCEPTION_PRIV_INSTRUCTION
, "EXCEPTION_PRIV_INSTRUCTION"},
64 {EXCEPTION_SINGLE_STEP
, "EXCEPTION_SINGLE_STEP"},
65 {EXCEPTION_STACK_OVERFLOW
, "EXCEPTION_STACK_OVERFLOW"},
66 {STATUS_UNWIND_CONSOLIDATE
, "STATUS_UNWIND_CONSOLIDATE"},
70 * Forcefully try to terminate the application.
72 * @param exit_code The exit code to return.
74 [[noreturn
]] static void ImmediateExitProcess(uint exit_code
)
76 /* TerminateProcess may fail in some special edge cases; fall back to ExitProcess in this case. */
77 TerminateProcess(GetCurrentProcess(), exit_code
);
78 ExitProcess(exit_code
);
82 * Windows implementation for the crash logger.
84 class CrashLogWindows
: public CrashLog
{
85 /** Information about the encountered exception */
86 EXCEPTION_POINTERS
*ep
;
88 void SurveyCrash(nlohmann::json
&survey
) const override
90 survey
["id"] = ep
->ExceptionRecord
->ExceptionCode
;
91 if (exception_code_to_name
.count(ep
->ExceptionRecord
->ExceptionCode
) > 0) {
92 survey
["reason"] = exception_code_to_name
.at(ep
->ExceptionRecord
->ExceptionCode
);
94 survey
["reason"] = "Unknown exception code";
98 void SurveyStacktrace(nlohmann::json
&survey
) const override
;
101 #ifdef WITH_UNOFFICIAL_BREAKPAD
102 static bool MinidumpCallback(const wchar_t *dump_dir
, const wchar_t *minidump_id
, void *context
, EXCEPTION_POINTERS
*, MDRawAssertionInfo
*, bool succeeded
)
104 CrashLogWindows
*crashlog
= reinterpret_cast<CrashLogWindows
*>(context
);
106 crashlog
->crashdump_filename
= crashlog
->CreateFileName(".dmp");
107 std::rename(fmt::format("{}/{}.dmp", FS2OTTD(dump_dir
), FS2OTTD(minidump_id
)).c_str(), crashlog
->crashdump_filename
.c_str());
111 bool WriteCrashDump() override
113 return google_breakpad::ExceptionHandler::WriteMinidump(OTTD2FS(_personal_dir
), MinidumpCallback
, this);
117 #if defined(_MSC_VER)
118 /* virtual */ bool TryExecute(std::string_view section_name
, std::function
<bool()> &&func
) override
120 this->try_execute_active
= true;
125 } __except (EXCEPTION_EXECUTE_HANDLER
) {
126 fmt::print("Something went wrong when attempting to fill {} section of the crash log.\n", section_name
);
130 this->try_execute_active
= false;
134 /* virtual */ bool TryExecute(std::string_view section_name
, std::function
<bool()> &&func
) override
136 this->try_execute_active
= true;
138 /* Setup a longjump in case a crash happens. */
139 if (setjmp(this->internal_fault_jmp_buf
) != 0) {
140 fmt::print("Something went wrong when attempting to fill {} section of the crash log.\n", section_name
);
142 this->try_execute_active
= false;
147 this->try_execute_active
= false;
150 #endif /* _MSC_VER */
153 * A crash log is always generated when it's generated.
154 * @param ep the data related to the exception.
156 CrashLogWindows(EXCEPTION_POINTERS
*ep
= nullptr) :
161 #if !defined(_MSC_VER)
162 /** Buffer to track the long jump set setup. */
163 jmp_buf internal_fault_jmp_buf
;
166 /** Whether we are in a TryExecute block. */
167 bool try_execute_active
= false;
169 /** Points to the current crash log. */
170 static CrashLogWindows
*current
;
173 /* static */ CrashLogWindows
*CrashLogWindows::current
= nullptr;
175 #if defined(_MSC_VER)
176 static const uint MAX_SYMBOL_LEN
= 512;
177 static const uint MAX_FRAMES
= 64;
179 /* virtual */ void CrashLogWindows::SurveyStacktrace(nlohmann::json
&survey
) const
181 LibraryLoader
dbghelp("dbghelp.dll");
183 BOOL (WINAPI
* pSymInitialize
)(HANDLE
, PCSTR
, BOOL
);
184 BOOL (WINAPI
* pSymSetOptions
)(DWORD
);
185 BOOL (WINAPI
* pSymCleanup
)(HANDLE
);
186 BOOL (WINAPI
* pStackWalk64
)(DWORD
, HANDLE
, HANDLE
, LPSTACKFRAME64
, PVOID
, PREAD_PROCESS_MEMORY_ROUTINE64
, PFUNCTION_TABLE_ACCESS_ROUTINE64
, PGET_MODULE_BASE_ROUTINE64
, PTRANSLATE_ADDRESS_ROUTINE64
);
187 PVOID (WINAPI
* pSymFunctionTableAccess64
)(HANDLE
, DWORD64
);
188 DWORD64 (WINAPI
* pSymGetModuleBase64
)(HANDLE
, DWORD64
);
189 BOOL (WINAPI
* pSymGetModuleInfo64
)(HANDLE
, DWORD64
, PIMAGEHLP_MODULE64
);
190 BOOL (WINAPI
* pSymGetSymFromAddr64
)(HANDLE
, DWORD64
, PDWORD64
, PIMAGEHLP_SYMBOL64
);
191 BOOL (WINAPI
* pSymGetLineFromAddr64
)(HANDLE
, DWORD64
, PDWORD
, PIMAGEHLP_LINE64
);
193 dbghelp
.GetFunction("SymInitialize"),
194 dbghelp
.GetFunction("SymSetOptions"),
195 dbghelp
.GetFunction("SymCleanup"),
196 dbghelp
.GetFunction("StackWalk64"),
197 dbghelp
.GetFunction("SymFunctionTableAccess64"),
198 dbghelp
.GetFunction("SymGetModuleBase64"),
199 dbghelp
.GetFunction("SymGetModuleInfo64"),
200 dbghelp
.GetFunction("SymGetSymFromAddr64"),
201 dbghelp
.GetFunction("SymGetLineFromAddr64"),
204 survey
= nlohmann::json::array();
206 /* Try to load the functions from the DLL, if that fails because of a too old dbghelp.dll, just skip it. */
207 if (!dbghelp
.HasError()) {
208 /* Initialize symbol handler. */
209 HANDLE hCur
= GetCurrentProcess();
210 proc
.pSymInitialize(hCur
, nullptr, TRUE
);
211 /* Load symbols only when needed, fail silently on errors, demangle symbol names. */
212 proc
.pSymSetOptions(SYMOPT_DEFERRED_LOADS
| SYMOPT_FAIL_CRITICAL_ERRORS
| SYMOPT_UNDNAME
);
214 /* Initialize starting stack frame from context record. */
216 memset(&frame
, 0, sizeof(frame
));
218 frame
.AddrPC
.Offset
= ep
->ContextRecord
->Rip
;
219 frame
.AddrFrame
.Offset
= ep
->ContextRecord
->Rbp
;
220 frame
.AddrStack
.Offset
= ep
->ContextRecord
->Rsp
;
221 #elif defined(_M_IX86)
222 frame
.AddrPC
.Offset
= ep
->ContextRecord
->Eip
;
223 frame
.AddrFrame
.Offset
= ep
->ContextRecord
->Ebp
;
224 frame
.AddrStack
.Offset
= ep
->ContextRecord
->Esp
;
225 #elif defined(_M_ARM64)
226 frame
.AddrPC
.Offset
= ep
->ContextRecord
->Pc
;
227 frame
.AddrFrame
.Offset
= ep
->ContextRecord
->Fp
;
228 frame
.AddrStack
.Offset
= ep
->ContextRecord
->Sp
;
230 frame
.AddrPC
.Mode
= AddrModeFlat
;
231 frame
.AddrFrame
.Mode
= AddrModeFlat
;
232 frame
.AddrStack
.Mode
= AddrModeFlat
;
234 /* Copy context record as StackWalk64 may modify it. */
236 memcpy(&ctx
, ep
->ContextRecord
, sizeof(ctx
));
238 /* Allocate space for symbol info. */
239 char sym_info_raw
[sizeof(IMAGEHLP_SYMBOL64
) + MAX_SYMBOL_LEN
- 1];
240 IMAGEHLP_SYMBOL64
*sym_info
= (IMAGEHLP_SYMBOL64
*)sym_info_raw
;
241 sym_info
->SizeOfStruct
= sizeof(IMAGEHLP_SYMBOL64
);
242 sym_info
->MaxNameLength
= MAX_SYMBOL_LEN
;
244 /* Walk stack at most MAX_FRAMES deep in case the stack is corrupt. */
245 for (uint num
= 0; num
< MAX_FRAMES
; num
++) {
246 if (!proc
.pStackWalk64(
248 IMAGE_FILE_MACHINE_AMD64
,
250 IMAGE_FILE_MACHINE_I386
,
252 hCur
, GetCurrentThread(), &frame
, &ctx
, nullptr, proc
.pSymFunctionTableAccess64
, proc
.pSymGetModuleBase64
, nullptr)) break;
254 if (frame
.AddrPC
.Offset
== frame
.AddrReturn
.Offset
) {
255 survey
.push_back("<infinite loop>");
259 /* Get module name. */
260 const char *mod_name
= "???";
262 IMAGEHLP_MODULE64 module
;
263 module
.SizeOfStruct
= sizeof(module
);
264 if (proc
.pSymGetModuleInfo64(hCur
, frame
.AddrPC
.Offset
, &module
)) {
265 mod_name
= module
.ModuleName
;
268 /* Print module and instruction pointer. */
269 std::string message
= fmt::format("{:20s} {:X}", mod_name
, frame
.AddrPC
.Offset
);
271 /* Get symbol name and line info if possible. */
273 if (proc
.pSymGetSymFromAddr64(hCur
, frame
.AddrPC
.Offset
, &offset
, sym_info
)) {
274 message
+= fmt::format(" {} + {}", sym_info
->Name
, offset
);
277 IMAGEHLP_LINE64 line
;
278 line
.SizeOfStruct
= sizeof(IMAGEHLP_LINE64
);
279 if (proc
.pSymGetLineFromAddr64(hCur
, frame
.AddrPC
.Offset
, &line_offs
, &line
)) {
280 message
+= fmt::format(" ({}:{})", line
.FileName
, line
.LineNumber
);
284 survey
.push_back(message
);
287 proc
.pSymCleanup(hCur
);
291 /* virtual */ void CrashLogWindows::SurveyStacktrace(nlohmann::json
&) const
295 #endif /* _MSC_VER */
297 extern bool CloseConsoleLogIfActive();
298 static void ShowCrashlogWindow();
301 * Stack pointer for use when 'starting' the crash handler.
302 * Not static as gcc's inline assembly needs it that way.
304 thread_local
void *_safe_esp
= nullptr;
306 static LONG WINAPI
ExceptionHandler(EXCEPTION_POINTERS
*ep
)
308 /* Restore system timer resolution. */
311 /* Disable our event loop. */
312 SetWindowLongPtr(GetActiveWindow(), GWLP_WNDPROC
, (LONG_PTR
)&DefWindowProc
);
314 if (CrashLogWindows::current
!= nullptr) {
315 CrashLog::AfterCrashLogCleanup();
316 ImmediateExitProcess(2);
319 if (_gamelog
.TestEmergency()) {
320 static const wchar_t _emergency_crash
[] =
321 L
"A serious fault condition occurred in the game. The game will shut down.\n"
322 L
"As you loaded an emergency savegame no crash information will be generated.\n";
323 MessageBox(nullptr, _emergency_crash
, L
"Fatal Application Failure", MB_ICONERROR
);
324 ImmediateExitProcess(3);
327 if (SaveloadCrashWithMissingNewGRFs()) {
328 static const wchar_t _saveload_crash
[] =
329 L
"A serious fault condition occurred in the game. The game will shut down.\n"
330 L
"As you loaded an savegame for which you do not have the required NewGRFs\n"
331 L
"no crash information will be generated.\n";
332 MessageBox(nullptr, _saveload_crash
, L
"Fatal Application Failure", MB_ICONERROR
);
333 ImmediateExitProcess(3);
336 CrashLogWindows
*log
= new CrashLogWindows(ep
);
337 CrashLogWindows::current
= log
;
340 /* Close any possible log files */
341 CloseConsoleLogIfActive();
343 if ((VideoDriver::GetInstance() == nullptr || VideoDriver::GetInstance()->HasGUI()) && _safe_esp
!= nullptr) {
345 ep
->ContextRecord
->Rip
= (DWORD64
)ShowCrashlogWindow
;
346 ep
->ContextRecord
->Rsp
= (DWORD64
)_safe_esp
;
347 #elif defined(_M_IX86)
348 ep
->ContextRecord
->Eip
= (DWORD
)ShowCrashlogWindow
;
349 ep
->ContextRecord
->Esp
= (DWORD
)_safe_esp
;
350 #elif defined(_M_ARM64)
351 ep
->ContextRecord
->Pc
= (DWORD64
)ShowCrashlogWindow
;
352 ep
->ContextRecord
->Sp
= (DWORD64
)_safe_esp
;
354 return EXCEPTION_CONTINUE_EXECUTION
;
357 CrashLog::AfterCrashLogCleanup();
358 return EXCEPTION_EXECUTE_HANDLER
;
361 static LONG WINAPI
VectoredExceptionHandler(EXCEPTION_POINTERS
*ep
)
363 if (CrashLogWindows::current
!= nullptr && CrashLogWindows::current
->try_execute_active
) {
364 #if defined(_MSC_VER)
365 return EXCEPTION_CONTINUE_SEARCH
;
367 longjmp(CrashLogWindows::current
->internal_fault_jmp_buf
, 1);
371 if (ep
->ExceptionRecord
->ExceptionCode
== 0xC0000374 /* heap corruption */) {
372 return ExceptionHandler(ep
);
374 if (ep
->ExceptionRecord
->ExceptionCode
== EXCEPTION_STACK_OVERFLOW
) {
375 return ExceptionHandler(ep
);
377 if (ep
->ExceptionRecord
->ExceptionCode
== CUSTOM_ABORT_EXCEPTION
) {
378 return ExceptionHandler(ep
);
381 return EXCEPTION_CONTINUE_SEARCH
;
384 static void CDECL
CustomAbort(int)
386 RaiseException(CUSTOM_ABORT_EXCEPTION
, 0, 0, nullptr);
389 /* static */ void CrashLog::InitialiseCrashLog()
391 CrashLog::InitThread();
393 /* SIGABRT is not an unhandled exception, so we need to intercept it. */
394 signal(SIGABRT
, CustomAbort
);
395 #if defined(_MSC_VER)
396 /* Don't show abort message as we will get the crashlog window anyway. */
397 _set_abort_behavior(0, _WRITE_ABORT_MSG
);
399 SetUnhandledExceptionFilter(ExceptionHandler
);
400 AddVectoredExceptionHandler(1, VectoredExceptionHandler
);
403 /* static */ void CrashLog::InitThread()
405 #if defined(_M_AMD64) || defined(_M_ARM64)
407 RtlCaptureContext(&ctx
);
409 /* The stack pointer for AMD64 must always be 16-byte aligned inside a
410 * function. As we are simulating a function call with the safe ESP value,
411 * we need to subtract 8 for the imaginary return address otherwise stack
412 * alignment would be wrong in the called function. */
413 # if defined(_M_ARM64)
414 _safe_esp
= (void *)(ctx
.Sp
- 8);
416 _safe_esp
= (void *)(ctx
.Rsp
- 8);
420 # if defined(_MSC_VER)
425 asm("movl %%esp, %0" : "=rm" (safe_esp
));
427 _safe_esp
= safe_esp
;
431 /* The crash log GUI */
433 static bool _expanded
;
435 static const wchar_t _crash_desc
[] =
436 L
"A serious fault condition occurred in the game. The game will shut down.\n"
437 L
"Please send crash.json.log, crash.dmp, and crash.sav to the developers.\n"
438 L
"This will greatly help debugging.\n\n"
439 L
"https://github.com/OpenTTD/OpenTTD/issues\n\n"
442 static const wchar_t * const _expand_texts
[] = {L
"S&how report >>", L
"&Hide report <<" };
444 static void SetWndSize(HWND wnd
, int mode
)
448 GetWindowRect(wnd
, &r
);
449 SetDlgItemText(wnd
, 15, _expand_texts
[mode
== 1]);
452 GetWindowRect(GetDlgItem(wnd
, 11), &r2
);
453 int offs
= r2
.bottom
- r2
.top
+ 10;
454 if (mode
== 0) offs
= -offs
;
455 SetWindowPos(wnd
, HWND_TOPMOST
, 0, 0,
456 r
.right
- r
.left
, r
.bottom
- r
.top
+ offs
, SWP_NOMOVE
| SWP_NOZORDER
);
458 SetWindowPos(wnd
, HWND_TOPMOST
,
459 (GetSystemMetrics(SM_CXSCREEN
) - (r
.right
- r
.left
)) / 2,
460 (GetSystemMetrics(SM_CYSCREEN
) - (r
.bottom
- r
.top
)) / 2,
465 static INT_PTR CALLBACK
CrashDialogFunc(HWND wnd
, UINT msg
, WPARAM wParam
, LPARAM
)
468 case WM_INITDIALOG
: {
469 std::string crashlog
= CrashLogWindows::current
->survey
.dump(4);
470 size_t crashlog_length
= crashlog
.size() + 1;
471 /* Reserve extra space for LF to CRLF conversion. */
472 crashlog_length
+= std::count(crashlog
.begin(), crashlog
.end(), '\n');
474 const size_t filename_count
= 4;
475 const size_t filename_buf_length
= MAX_PATH
+ 1;
476 const size_t crash_desc_buf_length
= lengthof(_crash_desc
) + filename_buf_length
* filename_count
+ 1;
478 /* We need to put the crash-log in a separate buffer because the default
479 * buffer in MB_TO_WIDE is not large enough (512 chars).
480 * Use VirtualAlloc to allocate pages for the buffer to avoid overflowing the stack.
481 * Avoid the heap in case the crash is because the heap became corrupted. */
482 const size_t total_length
= crash_desc_buf_length
* sizeof(wchar_t) +
483 crashlog_length
* sizeof(wchar_t) +
484 filename_buf_length
* sizeof(wchar_t) * filename_count
+
486 void *raw_buffer
= VirtualAlloc(nullptr, total_length
, MEM_COMMIT
| MEM_RESERVE
, PAGE_READWRITE
);
488 wchar_t *crash_desc_buf
= reinterpret_cast<wchar_t *>(raw_buffer
);
489 wchar_t *crashlog_buf
= crash_desc_buf
+ crash_desc_buf_length
;
490 wchar_t *filename_buf
= crashlog_buf
+ crashlog_length
;
491 char *crashlog_dos_nl
= reinterpret_cast<char *>(filename_buf
+ filename_buf_length
* filename_count
);
493 /* Convert unix -> dos newlines because the edit box only supports that properly. */
494 const char *crashlog_unix_nl
= crashlog
.data();
495 char *p
= crashlog_dos_nl
;
497 while ((c
= Utf8Consume(&crashlog_unix_nl
))) {
498 if (c
== '\n') p
+= Utf8Encode(p
, '\r');
499 p
+= Utf8Encode(p
, c
);
505 crash_desc_buf_length
,
507 convert_to_fs(CrashLogWindows::current
->crashlog_filename
, {filename_buf
+ filename_buf_length
* 0, filename_buf_length
}),
508 convert_to_fs(CrashLogWindows::current
->crashdump_filename
, {filename_buf
+ filename_buf_length
* 1, filename_buf_length
}),
509 convert_to_fs(CrashLogWindows::current
->savegame_filename
, {filename_buf
+ filename_buf_length
* 2, filename_buf_length
}),
510 convert_to_fs(CrashLogWindows::current
->screenshot_filename
, {filename_buf
+ filename_buf_length
* 3, filename_buf_length
})
513 SetDlgItemText(wnd
, 10, crash_desc_buf
);
514 SetDlgItemText(wnd
, 11, convert_to_fs(crashlog_dos_nl
, {crashlog_buf
, crashlog_length
}));
515 SendDlgItemMessage(wnd
, 11, WM_SETFONT
, (WPARAM
)GetStockObject(ANSI_FIXED_FONT
), FALSE
);
521 CrashLog::AfterCrashLogCleanup();
522 ImmediateExitProcess(2);
523 case 15: // Expand window to show crash-message
524 _expanded
= !_expanded
;
525 SetWndSize(wnd
, _expanded
);
530 CrashLog::AfterCrashLogCleanup();
531 ImmediateExitProcess(2);
537 static void ShowCrashlogWindow()
540 ShowWindow(GetActiveWindow(), FALSE
);
541 DialogBox(GetModuleHandle(nullptr), MAKEINTRESOURCE(100), nullptr, CrashDialogFunc
);