From f6a7b4c88d7c93913a22b319b80a66f45bb87292 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Fri, 10 Jun 2022 18:14:59 +0200 Subject: [PATCH] Show a GUI alert when crashing on desktop platforms This makes it possible to know that Godot has crashed without looking at the console (which may not be visible to the user). - Change the window title while dumping the backtrace, as this can take a while to finish. - If file logging is enabled, print path to log file to standard error and the GUI alert. Clicking OK will open the log file in the default program associated with `.log` files. Notepad has native associations for `.log` on Windows, so this works on Windows too. Co-authored-by: bruvzg <7645683+bruvzg@users.noreply.github.com> --- platform/linuxbsd/crash_handler_linuxbsd.cpp | 30 ++++++++++++- platform/macos/crash_handler_macos.mm | 42 ++++++++++++++++++- .../windows/crash_handler_windows_seh.cpp | 30 ++++++++++++- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index 446fe5c7a1e6..cbbf21baf6bf 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -35,6 +35,7 @@ #include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" +#include "servers/display_server.h" #ifndef DEBUG_ENABLED #undef CRASH_HANDLER_ENABLED @@ -62,9 +63,13 @@ static void handle_crash(int sig) { String _execpath = OS::get_singleton()->get_executable_path(); String msg; + String log_path; const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); if (proj_settings) { - msg = proj_settings->get("debug/settings/crash_handler/message"); + msg = proj_settings->get_setting_with_override("debug/settings/crash_handler/message"); + if (proj_settings->get_setting_with_override("debug/file_logging/enable_file_logging")) { + log_path = proj_settings->globalize_path(proj_settings->get_setting_with_override("debug/file_logging/log_path")); + } } // Tell MainLoop about the crash. This can be handled by users too in Node. @@ -72,9 +77,12 @@ static void handle_crash(int sig) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Set window title while dumping the backtrace, as this can take 10+ seconds to finish. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumping backtrace..."); + // Dump the backtrace to stderr with a message to the user print_error("\n================================================================"); - print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, sig)); + print_error(vformat("%s: Program crashed with signal %d.", __FUNCTION__, sig)); // Print the engine version just before, so that people are reminded to include the version in backtrace reports. if (String(VERSION_HASH).is_empty()) { @@ -141,6 +149,24 @@ static void handle_crash(int sig) { print_error("-- END OF BACKTRACE --"); print_error("================================================================"); + // Notify the user that backtrace dumping is finished. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumped backtrace"); + + // Show alert so the user is aware of the crash, even if the engine wasn't started with visible stdout/stderr. + // This must be done after printing the backtrace to prevent the process from being blocked too early (`OS::alert()` is blocking). + if (!log_path.is_empty()) { + // `fprintf()` is used instead of `print_error()`, as this printed line must not be present + // in the log file (it references the log file itself). + fprintf(stderr, "\nFind the log file for this session at:\n%s\n\n", log_path.utf8().get_data()); + + if (DisplayServer::get_singleton()->get_name() != "headless") { + OS::get_singleton()->alert(vformat("%s: Program crashed with signal %d.\n\nFind the log file for this session at:\n%s\n\nClicking OK will open this log file. Please include the this log file's contents in bug reports.", __FUNCTION__, sig, log_path), "Crash"); + OS::get_singleton()->shell_open(log_path.utf8().get_data()); + } + } else if (DisplayServer::get_singleton()->get_name() != "headless") { + OS::get_singleton()->alert(vformat("%s: Program crashed with signal %d.", __FUNCTION__, sig), "Crash"); + } + // Abort to pass the error to the OS abort(); } diff --git a/platform/macos/crash_handler_macos.mm b/platform/macos/crash_handler_macos.mm index c370422bfa73..5d38f71962db 100644 --- a/platform/macos/crash_handler_macos.mm +++ b/platform/macos/crash_handler_macos.mm @@ -35,6 +35,7 @@ #include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" +#include "servers/display_server.h" #include #include @@ -86,9 +87,13 @@ static void handle_crash(int sig) { String _execpath = OS::get_singleton()->get_executable_path(); String msg; + String log_path; const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); if (proj_settings) { - msg = proj_settings->get("debug/settings/crash_handler/message"); + msg = proj_settings->get_setting_with_override("debug/settings/crash_handler/message"); + if (proj_settings->get_setting_with_override("debug/file_logging/enable_file_logging")) { + log_path = proj_settings->globalize_path(proj_settings->get_setting_with_override("debug/file_logging/log_path")); + } } // Tell MainLoop about the crash. This can be handled by users too in Node. @@ -96,9 +101,12 @@ static void handle_crash(int sig) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Set window title while dumping the backtrace, as this can take 10+ seconds to finish. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumping backtrace..."); + // Dump the backtrace to stderr with a message to the user print_error("\n================================================================"); - print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, sig)); + print_error(vformat("%s: Program crashed with signal %d.", __FUNCTION__, sig)); // Print the engine version just before, so that people are reminded to include the version in backtrace reports. if (String(VERSION_HASH).is_empty()) { @@ -172,6 +180,36 @@ static void handle_crash(int sig) { print_error("-- END OF BACKTRACE --"); print_error("================================================================"); + // Notify the user that backtrace dumping is finished. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumped backtrace"); + + // Show alert so the user is aware of the crash, even if the engine wasn't started with visible stdout/stderr. + // This must be done after printing the backtrace to prevent the process from being blocked too early (`OS::alert()` is blocking). + if (!log_path.is_empty()) { + // `fprintf()` is used instead of `print_error()`, as this printed line must not be present + // in the log file (it references the log file itself). + fprintf(stderr, "\nFind the log file for this session at:\n%s\n\n", log_path.utf8().get_data()); + + if (DisplayServer::get_singleton()->get_name() != "headless") { + // Use AppleScript in `OS::execute()` instead of `OS::alert()` so that it works even when the OS event loop is blocked by the crash handler. + // (this is only required on macOS). + List args; + args.push_back("-e"); + args.push_back(vformat("display alert \"Crash\" message \"%s: Program crashed with signal %d.\\n\\nFind the log file for this session at:\\n%s\\n\\nClicking OK will open this log file. Please include the this log file's contents in bug reports.\"", __FUNCTION__, sig, log_path.c_escape())); + OS::get_singleton()->execute("osascript", args); + args.clear(); + args.push_back(log_path); + // Use `OS::execute()` instead of `OS::shell_open()` so that it works even when the OS event loop is blocked by the crash handler. + OS::get_singleton()->execute("open", args); + } + } else if (DisplayServer::get_singleton()->get_name() != "headless") { + // Use AppleScript in `OS::execute()` instead of `OS::alert()` so that it works even when the OS event loop is blocked by the crash handler. + List args; + args.push_back("-e"); + args.push_back(vformat("display alert \"Crash\" message \"%s: Program crashed with signal %d.\"", __FUNCTION__, sig)); + OS::get_singleton()->execute("osascript", args); + } + // Abort to pass the error to the OS abort(); } diff --git a/platform/windows/crash_handler_windows_seh.cpp b/platform/windows/crash_handler_windows_seh.cpp index 2abe285d3101..0156c7f1328e 100644 --- a/platform/windows/crash_handler_windows_seh.cpp +++ b/platform/windows/crash_handler_windows_seh.cpp @@ -35,6 +35,7 @@ #include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" +#include "servers/display_server.h" #ifdef CRASH_HANDLER_EXCEPTION @@ -128,9 +129,13 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { } String msg; + String log_path; const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); if (proj_settings) { - msg = proj_settings->get("debug/settings/crash_handler/message"); + msg = proj_settings->get_setting_with_override("debug/settings/crash_handler/message"); + if (proj_settings->get_setting_with_override("debug/file_logging/enable_file_logging")) { + log_path = proj_settings->globalize_path(proj_settings->get_setting_with_override("debug/file_logging/log_path")); + } } // Tell MainLoop about the crash. This can be handled by users too in Node. @@ -138,8 +143,11 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Set window title while dumping the backtrace, as this can take 10+ seconds to finish. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumping backtrace..."); + print_error("\n================================================================"); - print_error(vformat("%s: Program crashed", __FUNCTION__)); + print_error(vformat("%s: Program crashed.", __FUNCTION__)); // Print the engine version just before, so that people are reminded to include the version in backtrace reports. if (String(VERSION_HASH).is_empty()) { @@ -223,6 +231,24 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { print_error("-- END OF BACKTRACE --"); print_error("================================================================"); + // Notify the user that backtrace dumping is finished. + DisplayServer::get_singleton()->window_set_title("ERROR: Program crashed, dumped backtrace"); + + // Show alert so the user is aware of the crash, even if the engine wasn't started with visible stdout/stderr. + // This must be done after printing the backtrace to prevent the process from being blocked too early (`OS::alert()` is blocking). + if (!log_path.is_empty()) { + // `fprintf()` is used instead of `print_error()`, as this printed line must not be present + // in the log file (it references the log file itself). + fprintf(stderr, "\nFind the log file for this session at:\n%s\n\n", log_path.utf8().get_data()); + + if (DisplayServer::get_singleton()->get_name() != "headless") { + OS::get_singleton()->alert(vformat("%s: Program crashed.\n\nFind the log file for this session at:\n%s\n\nClicking OK will open this log file. Please include the this log file's contents in bug reports.", __FUNCTION__, log_path), "Crash"); + OS::get_singleton()->shell_open(log_path.utf8().get_data()); + } + } else if (DisplayServer::get_singleton()->get_name() != "headless") { + OS::get_singleton()->alert(vformat("%s: Program crashed.", __FUNCTION__), "Crash"); + } + SymCleanup(process); // Pass the exception to the OS