diff --git a/meson.build b/meson.build index 3c9cf130b..2530bcea6 100644 --- a/meson.build +++ b/meson.build @@ -361,6 +361,7 @@ else ident_platform = 'UNKNOWN' endif +project_deps = [] data_files = [] powder_deps = [] @@ -378,7 +379,7 @@ if host_platform == 'emscripten' endif if get_option('build_powder') - powder_deps += [ + powder_deps += project_deps + [ threads_dep, zlib_dep, png_dep, @@ -419,7 +420,7 @@ if get_option('build_render') if host_platform == 'emscripten' error('render does not target emscripten') endif - render_deps = [ + render_deps = project_deps + [ threads_dep, zlib_dep, bzip2_dep, @@ -445,7 +446,7 @@ if get_option('build_font') if host_platform == 'emscripten' error('font does not target emscripten') endif - font_deps = [ + font_deps = project_deps + [ threads_dep, zlib_dep, png_dep, diff --git a/meson_options.txt b/meson_options.txt index 5f8c06c2d..1e7e32ed5 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -286,3 +286,10 @@ option( value: true, description: 'Enable platform clipboard, allows copying simulation data between different windows' ) +option( + 'use_bluescreen', + type: 'combo', + choices: [ 'no', 'yes', 'auto' ], + value: 'auto', + description: 'Show blue error screen upon unhandled signals and exceptions' +) diff --git a/src/Format.cpp b/src/Format.cpp index 1d79b98c4..c9d455e85 100644 --- a/src/Format.cpp +++ b/src/Format.cpp @@ -10,12 +10,19 @@ #include "Format.h" #include "graphics/Graphics.h" -ByteString format::UnixtimeToDate(time_t unixtime, ByteString dateFormat) +ByteString format::UnixtimeToDate(time_t unixtime, ByteString dateFormat, bool local) { struct tm * timeData; char buffer[128]; - timeData = localtime(&unixtime); + if (local) + { + timeData = localtime(&unixtime); + } + else + { + timeData = gmtime(&unixtime); + } strftime(buffer, 128, dateFormat.c_str(), timeData); return ByteString(buffer); diff --git a/src/Format.h b/src/Format.h index 87c2aefab..b406d13fb 100644 --- a/src/Format.h +++ b/src/Format.h @@ -11,7 +11,7 @@ namespace format { ByteString URLEncode(ByteString value); ByteString URLDecode(ByteString value); - ByteString UnixtimeToDate(time_t unixtime, ByteString dateFomat = ByteString("%d %b %Y")); + ByteString UnixtimeToDate(time_t unixtime, ByteString dateFomat = ByteString("%d %b %Y"), bool local = true); ByteString UnixtimeToDateMini(time_t unixtime); String CleanString(String dirtyString, bool ascii, bool color, bool newlines, bool numeric = false); std::vector PixelsToPPM(PlaneAdapter> const &); diff --git a/src/PowderToy.cpp b/src/PowderToy.cpp index 39e62dba4..cd246fb3b 100644 --- a/src/PowderToy.cpp +++ b/src/PowderToy.cpp @@ -97,25 +97,46 @@ void TickClient() Client::Ref().Tick(); } -void BlueScreen(String detailMessage) +void BlueScreen(String detailMessage, std::optional> stackTrace) { auto &engine = ui::Engine::Ref(); engine.g->BlendFilledRect(engine.g->Size().OriginRect(), 0x1172A9_rgb .WithAlpha(0xD2)); - String errorTitle = "ERROR"; - String errorDetails = "Details: " + detailMessage; - String errorHelp = String("An unrecoverable fault has occurred, please report the error by visiting the website below\n") + SCHEME + SERVER; - auto versionInfo = ByteString::Build("Version: ", VersionInfo(), "\nTag: ", VCS_TAG).FromUtf8(); + String errorText; + auto addParapgraph = [&errorText](String str) { + errorText += str + "\n\n"; + }; - // We use the width of errorHelp to center, but heights of the individual texts for vertical spacing - auto pos = engine.g->Size() / 2 - Vec2(Graphics::TextSize(errorHelp).X / 2, 100); - engine.g->BlendText(pos, errorTitle, 0xFFFFFF_rgb .WithAlpha(0xFF)); - pos.Y += 4 + Graphics::TextSize(errorTitle).Y; - engine.g->BlendText(pos, errorDetails, 0xFFFFFF_rgb .WithAlpha(0xFF)); - pos.Y += 4 + Graphics::TextSize(errorDetails).Y; - engine.g->BlendText(pos, errorHelp, 0xFFFFFF_rgb .WithAlpha(0xFF)); - pos.Y += 4 + Graphics::TextSize(errorHelp).Y; - engine.g->BlendText(pos, versionInfo, 0xFFFFFF_rgb .WithAlpha(0xFF)); + auto crashPrevLogPath = ByteString("crash.prev.log"); + auto crashLogPath = ByteString("crash.log"); + Platform::RenameFile(crashLogPath, crashPrevLogPath, true); + + StringBuilder crashInfo; + crashInfo << "ERROR - Details: " << detailMessage << "\n"; + crashInfo << "An unrecoverable fault has occurred, please report it by visiting the website below\n\n " << SCHEME << SERVER << "\n\n"; + crashInfo << "An attempt will be made to save all of this information to " << crashLogPath.FromUtf8() << " in your data folder.\n"; + crashInfo << "Please attach this file to your report.\n\n"; + crashInfo << "Version: " << VersionInfo().FromUtf8() << "\n"; + crashInfo << "Tag: " << VCS_TAG << "\n"; + crashInfo << "Date: " << format::UnixtimeToDate(time(NULL), "%Y-%m-%dT%H:%M:%SZ", false).FromUtf8() << "\n"; + if (stackTrace) + { + crashInfo << "Stack trace:\n"; + for (auto &item : *stackTrace) + { + crashInfo << " - " << item << "\n"; + } + } + else + { + crashInfo << "Stack trace not available\n"; + } + addParapgraph(crashInfo.Build()); + + engine.g->BlendText(ui::Point((engine.g->Size().X - 440) / 2, 80), errorText, 0xFFFFFF_rgb .WithAlpha(0xFF)); + + auto crashLogData = errorText.ToUtf8(); + Platform::WriteFile(std::vector(crashLogData.begin(), crashLogData.end()), crashLogPath); //Death loop SDL_Event event; @@ -151,7 +172,7 @@ void SigHandler(int signal) break; } } - BlueScreen(ByteString(message).FromUtf8()); + BlueScreen(ByteString(message).FromUtf8(), Platform::StackTrace(Platform::stackTraceFromHere)); } constexpr int SCALE_MAXIMUM = 10; @@ -518,7 +539,7 @@ int Main(int argc, char *argv[]) } catch (const std::exception &e) { - BlueScreen(ByteString(e.what()).FromUtf8()); + BlueScreen(ByteString(e.what()).FromUtf8(), Platform::StackTrace(Platform::stackTraceFromException)); } } else diff --git a/src/common/Defer.h b/src/common/Defer.h new file mode 100644 index 000000000..2e7a70d34 --- /dev/null +++ b/src/common/Defer.h @@ -0,0 +1,23 @@ +#pragma once +#include + +class Defer +{ + std::function func; + +public: + Defer(std::function newFunc) : func(newFunc) + { + } + + Defer(const Defer &other) = delete; + Defer &operator =(const Defer &other) = delete; + + ~Defer() + { + if (func) + { + func(); + } + } +}; diff --git a/src/common/platform/Platform.h b/src/common/platform/Platform.h index 04eb2f34b..2b0d2e773 100644 --- a/src/common/platform/Platform.h +++ b/src/common/platform/Platform.h @@ -1,6 +1,8 @@ #pragma once #include "common/String.h" #include +#include +#include namespace Platform { @@ -67,6 +69,13 @@ namespace Platform int InvokeMain(int argc, char *argv[]); + enum StackTraceType + { + stackTraceFromHere, + stackTraceFromException, + }; + std::optional> StackTrace(StackTraceType StackTraceType); + void MarkPresentable(); } diff --git a/src/common/platform/meson.build b/src/common/platform/meson.build index ddb8a2cab..0856b35c2 100644 --- a/src/common/platform/meson.build +++ b/src/common/platform/meson.build @@ -2,7 +2,11 @@ common_files += files( 'Common.cpp', ) -use_bluescreen = not is_debug +if get_option('use_bluescreen') == 'auto' + use_bluescreen = not is_debug +else + use_bluescreen = get_option('use_bluescreen') == 'yes' +endif can_install_enforce_no = false set_window_icon = false path_sep_char = '/' @@ -73,6 +77,14 @@ else 'DdirCommon.cpp', ) endif + +subdir('stacktrace') + +if use_bluescreen + common_files += stacktrace_files +else + common_files += files('stacktrace/Null.cpp') +endif conf_data.set('SET_WINDOW_ICON', set_window_icon.to_string()) conf_data.set('PATH_SEP_CHAR', path_sep_char) conf_data.set('USE_BLUESCREEN', use_bluescreen.to_string()) diff --git a/src/common/platform/stacktrace/Execinfo.cpp b/src/common/platform/stacktrace/Execinfo.cpp new file mode 100644 index 000000000..eb42a0de3 --- /dev/null +++ b/src/common/platform/stacktrace/Execinfo.cpp @@ -0,0 +1,31 @@ +#include "common/platform/Platform.h" +#include "common/Defer.h" +#include +#include +#include + +namespace Platform +{ +std::optional> StackTrace(StackTraceType) +{ + std::array buf; + auto used = backtrace(&buf[0], buf.size()); + auto *strs = backtrace_symbols(&buf[0], used); + Defer freeStrs([strs]() { + free(strs); + }); + std::vector res; + for (auto i = 0; i < used; ++i) + { + if (strs) + { + res.push_back(ByteString(strs[i]).FromUtf8()); + } + else + { + res.push_back(String::Build("0x", Format::Hex(), uintptr_t(buf[i]))); + } + } + return res; +} +} diff --git a/src/common/platform/stacktrace/Null.cpp b/src/common/platform/stacktrace/Null.cpp new file mode 100644 index 000000000..35dfff12b --- /dev/null +++ b/src/common/platform/stacktrace/Null.cpp @@ -0,0 +1,9 @@ +#include "common/platform/Platform.h" + +namespace Platform +{ +std::optional> StackTrace(StackTraceType) +{ + return std::nullopt; +} +} diff --git a/src/common/platform/stacktrace/Windows.cpp b/src/common/platform/stacktrace/Windows.cpp new file mode 100644 index 000000000..224ab0ac6 --- /dev/null +++ b/src/common/platform/stacktrace/Windows.cpp @@ -0,0 +1,130 @@ +#include "common/platform/Platform.h" +#include "common/Defer.h" +#include +#pragma pack(push, 8) +#include +#pragma pack(pop) +#include +#include +#include + +#if defined(_MSC_VER) && _MSC_VER >= 1900 +extern "C" void **__cdecl __current_exception_context(); +#endif + +static CONTEXT *CurrentExceptionContext() +{ + // TODO: find the corresponding hack for mingw and older msvc; stack traces printed for exceptions are broken without it + CONTEXT **pctx = nullptr; +#if defined(_MSC_VER) && _MSC_VER >= 1900 + pctx = (CONTEXT **)__current_exception_context(); +#endif + return pctx ? *pctx : nullptr; +} + +namespace Platform +{ +std::optional> StackTrace(StackTraceType stackTraceType) +{ + static std::mutex mx; + std::unique_lock lk(mx); + auto process = GetCurrentProcess(); + auto thread = GetCurrentThread(); + + Defer symCleanup([process]() { + SymCleanup(process); + }); + SymInitialize(process, NULL, TRUE); + + CONTEXT context; + memset(&context, 0, sizeof(context)); + switch (stackTraceType) + { + case stackTraceFromException: + if (auto *ec = CurrentExceptionContext()) + { + if (ec->ContextFlags) + { + context = *ec; + } + } + if (!context.ContextFlags) + { + return std::nullopt; + } + break; + + default: + context.ContextFlags = CONTEXT_FULL; + RtlCaptureContext(&context); + break; + } + + STACKFRAME64 frame; + memset(&frame, 0, sizeof(frame)); + DWORD machine; +#if defined(_M_IX86) + machine = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = context.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Esp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_X64) + machine = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = context.Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Rsp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Rsp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_IA64) + machine = IMAGE_FILE_MACHINE_IA64; + frame.AddrPC.Offset = context.StIIP; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.IntSp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrBStore.Offset = context.RsBSP; + frame.AddrBStore.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.IntSp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_ARM64) + machine = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = context.Pc; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Fp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Sp; + frame.AddrStack.Mode = AddrModeFlat; +#else + return std::nullopt; +#endif + + std::vector res; + for (auto i = 0; i < 100; ++i) + { + if (!StackWalk64( machine, process, thread, &frame, &context, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) + { + break; + } + StringBuilder addr; + addr << "0x" << Format::Hex() << uintptr_t(frame.AddrPC.Offset); + std::array moduleBaseName; + HMODULE module; + MODULEINFO moduleInfo; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)frame.AddrPC.Offset, &module) && + GetModuleBaseNameW(process, module, &moduleBaseName[0], moduleBaseName.size()) && + GetModuleInformation(process, module, &moduleInfo, sizeof(moduleInfo))) + { + auto offset = uintptr_t(frame.AddrPC.Offset) - uintptr_t(moduleInfo.lpBaseOfDll); + if (offset < moduleInfo.SizeOfImage) + { + addr << " (" << WinNarrow(&moduleBaseName[0]).FromUtf8() << "+0x" << Format::Hex() << offset << ")"; + } + } + res.push_back(addr.Build()); + } + return res; +} +} diff --git a/src/common/platform/stacktrace/meson.build b/src/common/platform/stacktrace/meson.build new file mode 100644 index 000000000..0676a480d --- /dev/null +++ b/src/common/platform/stacktrace/meson.build @@ -0,0 +1,17 @@ +if host_platform == 'windows' + if use_bluescreen + project_deps += [ + c_compiler.find_library('dbghelp'), + c_compiler.find_library('psapi'), + ] + endif + stacktrace_files = files('Windows.cpp') +elif host_platform == 'darwin' + # stacktrace_files = files('Execinfo.cpp') # macos has this but it's useless >_> + stacktrace_files = files('Null.cpp') +elif host_platform == 'linux' + # TODO: again, this is more like "posix" than "linux" + stacktrace_files = files('Execinfo.cpp') +else + stacktrace_files = files('Null.cpp') +endif