Skip to content

Commit

Permalink
Show a GUI alert when crashing on desktop platforms
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Calinou and bruvzg committed May 7, 2024
1 parent 55b8724 commit f6a7b4c
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 6 deletions.
30 changes: 28 additions & 2 deletions platform/linuxbsd/crash_handler_linuxbsd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,19 +63,26 @@ 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.
if (OS::get_singleton()->get_main_loop()) {
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()) {
Expand Down Expand Up @@ -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();
}
Expand Down
42 changes: 40 additions & 2 deletions platform/macos/crash_handler_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "core/string/print_string.h"
#include "core/version.h"
#include "main/main.h"
#include "servers/display_server.h"

#include <string.h>
#include <unistd.h>
Expand Down Expand Up @@ -86,19 +87,26 @@ 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.
if (OS::get_singleton()->get_main_loop()) {
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()) {
Expand Down Expand Up @@ -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<String> 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<String> 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();
}
Expand Down
30 changes: 28 additions & 2 deletions platform/windows/crash_handler_windows_seh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -128,18 +129,25 @@ 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.
if (OS::get_singleton()->get_main_loop()) {
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()) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f6a7b4c

Please sign in to comment.