impr: Handle crashes that may happen before the main loop (#1115)

Draft because I absolutely do not trust myself writing good code at 2AM.
I will review it tomorrow
This commit is contained in:
iTrooz 2023-06-01 18:35:41 +02:00 committed by GitHub
parent 9dafdeb70a
commit 117832e007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 113 deletions

View File

@ -155,6 +155,12 @@ namespace hex {
};
/* Default Events */
/**
* @brief Called when Imhex finished startup, and will enter the main window rendering loop
*/
EVENT_DEF(EventImHexStartupFinished);
EVENT_DEF(EventFileLoaded, std::fs::path);
EVENT_DEF(EventDataChanged);
EVENT_DEF(EventHighlightingChanged);

View File

@ -2,6 +2,7 @@ project(main)
add_executable(main ${APPLICATION_TYPE}
source/main.cpp
source/crash_handlers.cpp
source/window/window.cpp
source/window/win_window.cpp

View File

@ -0,0 +1,6 @@
#pragma once
namespace hex::crash {
void setupCrashHandlers();
}

View File

@ -14,6 +14,8 @@ struct ImGuiSettingsHandler;
namespace hex {
std::fs::path getImGuiSettingsPath();
void nativeErrorMessage(const std::string &message);
class Window {
@ -59,8 +61,6 @@ namespace hex {
std::list<std::string> m_popupsToOpen;
std::vector<int> m_pressedKeys;
std::fs::path m_imguiSettingsPath;
bool m_mouseButtonDown = false;
bool m_hadEvent = false;

View File

@ -0,0 +1,152 @@
#include <hex/api/project_file_manager.hpp>
#include <hex/helpers/logger.hpp>
#include <hex/helpers/fs.hpp>
#include <hex/helpers/stacktrace.hpp>
#include <wolv/io/fs.hpp>
#include <wolv/utils/string.hpp>
#include "window.hpp"
#include <nlohmann/json.hpp>
#include <llvm/Demangle/Demangle.h>
#include <exception>
#include <csignal>
namespace hex::crash {
constexpr static auto CrashBackupFileName = "crash_backup.hexproj";
constexpr static auto Signals = {SIGSEGV, SIGILL, SIGABRT,SIGFPE};
static std::terminate_handler originalHandler;
static void sendNativeMessage(const std::string& message) {
hex::nativeErrorMessage(hex::format("ImHex crashed during its loading.\nError: {}", message));
}
// function that decides what should happen on a crash
// (either sending a message or saving a crash file, depending on when the crash occured)
static std::function<void(const std::string&)> crashCallback = sendNativeMessage;
static void saveCrashFile(const std::string& message) {
hex::unused(message);
nlohmann::json crashData{
{"logFile", wolv::util::toUTF8String(hex::log::getFile().getPath())},
{"project", wolv::util::toUTF8String(ProjectFile::getPath())},
};
for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Config)) {
wolv::io::File file(path / "crash.json", wolv::io::File::Mode::Write);
if (file.isValid()) {
file.writeString(crashData.dump(4));
file.close();
log::info("Wrote crash.json file to {}", wolv::util::toUTF8String(file.getPath()));
return;
}
}
log::warn("Could not write crash.json file !");
}
// Custom signal handler to print various information and a stacktrace when the application crashes
static void signalHandler(int signalNumber, const std::string &signalName) {
log::fatal("Terminating with signal '{}' ({})", signalName, signalNumber);
// trigger the crash callback
crashCallback(hex::format("Received signal '{}' ({})", signalName, signalNumber));
// Trigger an event so that plugins can handle crashes
// It may affect things (like the project path),
// so we do this after saving the crash file
EventManager::post<EventAbnormalTermination>(signalNumber);
// Detect if the crash was due to an uncaught exception
if (std::uncaught_exceptions() > 0) {
log::fatal("Uncaught exception thrown!");
}
// Reset the signal handler to the default handler
for(auto signal : Signals) std::signal(signal, SIG_DFL);
// Print stack trace
for (const auto &stackFrame : stacktrace::getStackTrace()) {
if (stackFrame.line == 0)
log::fatal(" {}", stackFrame.function);
else
log::fatal(" ({}:{}) | {}", stackFrame.file, stackFrame.line, stackFrame.function);
}
// Trigger a breakpoint if we're in a debug build or raise the signal again for the default handler to handle it
#if defined(DEBUG)
assert(!"Debug build, triggering breakpoint");
#else
std::raise(signalNumber);
#endif
}
// setup functions to handle signals, uncaught exception, or similar stuff that will crash ImHex
void setupCrashHandlers() {
// Register signal handlers
{
#define HANDLE_SIGNAL(name) \
std::signal(name, [](int signalNumber){ \
signalHandler(signalNumber, #name); \
})
HANDLE_SIGNAL(SIGSEGV);
HANDLE_SIGNAL(SIGILL);
HANDLE_SIGNAL(SIGABRT);
HANDLE_SIGNAL(SIGFPE);
#undef HANDLE_SIGNAL
}
originalHandler = std::set_terminate([]{
try {
std::rethrow_exception(std::current_exception());
} catch (std::exception &ex) {
std::string exceptionStr = hex::format("{}()::what() -> {}",
llvm::itaniumDemangle(typeid(ex).name(), nullptr, nullptr, nullptr), ex.what()
);
log::fatal("Program terminated with uncaught exception: {}", exceptionStr);
// handle crash callback
crashCallback(hex::format("Uncaught exception: {}", exceptionStr));
// reset signal handlers prior to calling the original handler, because it may raise a signal
for(auto signal : Signals) std::signal(signal, SIG_DFL);
// call the original handler of C++ std
originalHandler();
log::error("Should not happen: original std::set_terminate handler returned. Terminating manually");
exit(EXIT_FAILURE);
}
log::error("Should not happen: catch block should be executed and terminate the program. Terminating manually");
exit(EXIT_FAILURE);
});
// Save a backup project when the application crashes
// We need to save the project no mater if it is dirty,
// because this save is responsible for telling us which files
// were opened in case there wasn't a project
EventManager::subscribe<EventAbnormalTermination>([](int) {
auto imguiSettingsPath = hex::getImGuiSettingsPath();
if (!imguiSettingsPath.empty())
ImGui::SaveIniSettingsToDisk(wolv::util::toUTF8String(imguiSettingsPath).c_str());
for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Config)) {
if (ProjectFile::store(path / CrashBackupFileName))
break;
}
});
EventManager::subscribe<EventImHexStartupFinished>([]{
crashCallback = saveCrashFile;
});
}
}

View File

@ -3,9 +3,11 @@
#include <hex/helpers/logger.hpp>
#include "window.hpp"
#include "crash_handlers.hpp"
#include "init/splash_window.hpp"
#include "init/tasks.hpp"
#include "init/tasks.hpp"
#include <hex/api/task.hpp>
#include <hex/api/project_file_manager.hpp>
@ -15,6 +17,7 @@
int main(int argc, char **argv, char **envp) {
using namespace hex;
hex::crash::setupCrashHandlers();
ImHexApi::System::impl::setProgramArguments(argc, argv, envp);
// Check if ImHex is installed in portable mode
@ -79,6 +82,8 @@ int main(int argc, char **argv, char **envp) {
}
// Render the main window
EventManager::post<EventImHexStartupFinished>();
window.loop();
}

View File

@ -22,7 +22,6 @@
#include <cassert>
#include <romfs/romfs.hpp>
#include <llvm/Demangle/Demangle.h>
#include <imgui.h>
#define IMGUI_DEFINE_MATH_OPERATORS
@ -45,61 +44,13 @@ namespace hex {
using namespace std::literals::chrono_literals;
static void saveCrashFile(){
nlohmann::json crashData{
{"logFile", wolv::util::toUTF8String(hex::log::getFile().getPath())},
{"project", wolv::util::toUTF8String(ProjectFile::getPath())},
};
for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Config)) {
wolv::io::File file(path / "crash.json", wolv::io::File::Mode::Write);
if (file.isValid()) {
file.writeString(crashData.dump(4));
file.close();
log::info("Wrote crash.json file to {}", wolv::util::toUTF8String(file.getPath()));
return;
}
}
log::warn("Could not write crash.json file !");
}
static std::fs::path s_imguiSettingsPath;
// Custom signal handler to print various information and a stacktrace when the application crashes
static void signalHandler(int signalNumber, const std::string &signalName) {
log::fatal("Terminating with signal '{}' ({})", signalName, signalNumber);
// save crash.json file
saveCrashFile();
// Trigger an event so that plugins can handle crashes
// It may affect things (like the project path),
// so we do this after saving the crash file
EventManager::post<EventAbnormalTermination>(signalNumber);
// Detect if the crash was due to an uncaught exception
if (std::uncaught_exceptions() > 0) {
log::fatal("Uncaught exception thrown!");
}
// Reset the signal handler to the default handler
std::signal(SIGSEGV, SIG_DFL);
std::signal(SIGILL, SIG_DFL);
std::signal(SIGABRT, SIG_DFL);
std::signal(SIGFPE, SIG_DFL);
// Print stack trace
for (const auto &stackFrame : stacktrace::getStackTrace()) {
if (stackFrame.line == 0)
log::fatal(" {}", stackFrame.function);
else
log::fatal(" ({}:{}) | {}", stackFrame.file, stackFrame.line, stackFrame.function);
}
// Trigger a breakpoint if we're in a debug build or raise the signal again for the default handler to handle it
#if defined(DEBUG)
assert(!"Debug build, triggering breakpoint");
#else
std::raise(signalNumber);
#endif
/**
* @brief returns the path to load/save imgui settings to, or an empty path if no location was found
*/
std::fs::path getImGuiSettingsPath() {
return s_imguiSettingsPath;
}
Window::Window() {
@ -152,8 +103,6 @@ namespace hex {
this->exitGLFW();
}
static std::terminate_handler originalHandler;
void Window::registerEventHandlers() {
// Initialize default theme
EventManager::post<RequestChangeTheme>("Dark");
@ -197,60 +146,12 @@ namespace hex {
glfwSetWindowTitle(this->m_window, title.c_str());
});
constexpr static auto CrashBackupFileName = "crash_backup.hexproj";
// Save a backup project when the application crashes
// We need to save the project no mater if it is dirty,
// because this save is responsible for telling us which files
// were opened in case there wasn't a project
EventManager::subscribe<EventAbnormalTermination>(this, [this](int) {
ImGui::SaveIniSettingsToDisk(wolv::util::toUTF8String(this->m_imguiSettingsPath).c_str());
for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Config)) {
if (ProjectFile::store(path / CrashBackupFileName))
break;
}
});
// Handle opening popups
EventManager::subscribe<RequestOpenPopup>(this, [this](auto name) {
std::scoped_lock lock(this->m_popupMutex);
this->m_popupsToOpen.push_back(name);
});
// Register signal handlers
{
#define HANDLE_SIGNAL(name) \
std::signal(name, [](int signalNumber){ \
signalHandler(signalNumber, #name); \
})
HANDLE_SIGNAL(SIGSEGV);
HANDLE_SIGNAL(SIGILL);
HANDLE_SIGNAL(SIGABRT);
HANDLE_SIGNAL(SIGFPE);
#undef HANDLE_SIGNAL
}
originalHandler = std::set_terminate([]{
try {
std::rethrow_exception(std::current_exception());
} catch (std::exception &ex) {
log::fatal(
"Program terminated with uncaught exception: {}()::what() -> {}",
llvm::itaniumDemangle(typeid(ex).name(), nullptr, nullptr, nullptr),
ex.what()
);
}
// the handler should eventually release a signal, which will be caught and used to handle the crash
originalHandler();
log::error("Should not happen: original std::set_terminate handler returned. Terminating manually");
exit(EXIT_FAILURE);
});
}
void Window::loop() {
@ -1100,15 +1001,16 @@ namespace hex {
ImGui::GetCurrentContext()->SettingsHandlers.push_back(handler);
for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) {
if (std::fs::exists(dir) && fs::isPathWritable(dir)) {
this->m_imguiSettingsPath = dir / "interface.ini";
io.IniFilename = nullptr;
if (std::fs::exists(dir) && (fs::isPathWritable(dir))) {
s_imguiSettingsPath = dir / "interface.ini";
break;
}
}
if (!this->m_imguiSettingsPath.empty() && wolv::io::fs::exists(this->m_imguiSettingsPath))
ImGui::LoadIniSettingsFromDisk(wolv::util::toUTF8String(this->m_imguiSettingsPath).c_str());
if (!s_imguiSettingsPath.empty() && wolv::io::fs::exists(s_imguiSettingsPath)) {
io.IniFilename = nullptr;
ImGui::LoadIniSettingsFromDisk(wolv::util::toUTF8String(s_imguiSettingsPath).c_str());
}
}
ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);
@ -1135,7 +1037,7 @@ namespace hex {
void Window::exitImGui() {
delete static_cast<ImGui::ImHexCustomData *>(ImGui::GetIO().UserData);
ImGui::SaveIniSettingsToDisk(wolv::util::toUTF8String(this->m_imguiSettingsPath).c_str());
ImGui::SaveIniSettingsToDisk(wolv::util::toUTF8String(s_imguiSettingsPath).c_str());
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();