From 25add89b14824faf41e754d9761a4612fb4cde60 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 24 Dec 2023 15:38:58 +0100 Subject: [PATCH] feat: Add crash recovery on Windows (#5012) --- .clang-tidy | 1 + .github/workflows/build.yml | 4 +- .github/workflows/clang-tidy.yml | 2 +- .gitmodules | 6 +- CHANGELOG.md | 1 + CMakeLists.txt | 2 +- lib/crashpad | 1 - mocks/include/mocks/EmptyApplication.hpp | 5 + src/Application.cpp | 7 +- src/Application.hpp | 7 + src/CMakeLists.txt | 5 +- src/RunGui.cpp | 53 +--- src/common/Args.cpp | 124 ++++++++-- src/common/Args.hpp | 28 +++ src/common/FlagsEnum.hpp | 5 + src/common/QLogging.cpp | 2 + src/common/QLogging.hpp | 1 + src/main.cpp | 2 +- src/providers/Crashpad.cpp | 95 -------- src/providers/Crashpad.hpp | 14 -- src/singletons/CrashHandler.cpp | 230 ++++++++++++++++++ src/singletons/CrashHandler.hpp | 36 +++ src/singletons/Settings.hpp | 1 - src/widgets/dialogs/LastRunCrashDialog.cpp | 146 ++++++----- src/widgets/dialogs/LastRunCrashDialog.hpp | 4 - src/widgets/settingspages/GeneralPage.cpp | 11 +- src/widgets/settingspages/GeneralPageView.cpp | 22 ++ src/widgets/settingspages/GeneralPageView.hpp | 5 + tools/crash-handler | 1 + 29 files changed, 563 insertions(+), 258 deletions(-) delete mode 160000 lib/crashpad delete mode 100644 src/providers/Crashpad.cpp delete mode 100644 src/providers/Crashpad.hpp create mode 100644 src/singletons/CrashHandler.cpp create mode 100644 src/singletons/CrashHandler.hpp create mode 160000 tools/crash-handler diff --git a/.clang-tidy b/.clang-tidy index 658f66139..32bd6b420 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -29,6 +29,7 @@ Checks: "-*, -readability-function-cognitive-complexity, -bugprone-easily-swappable-parameters, -cert-err58-cpp, + -modernize-avoid-c-arrays " CheckOptions: - key: readability-identifier-naming.ClassCase diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54340bb84..7f9cdd097 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -230,9 +230,9 @@ jobs: run: | cd build set cl=/MP - nmake /S /NOLOGO crashpad_handler + nmake /S /NOLOGO chatterino-crash-handler mkdir Chatterino2/crashpad - cp bin/crashpad/crashpad_handler.exe Chatterino2/crashpad/crashpad_handler.exe + cp bin/crashpad/crashpad-handler.exe Chatterino2/crashpad/crashpad-handler.exe 7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb - name: Prepare build dir (windows) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 96468c137..95a832a7f 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -124,7 +124,7 @@ jobs: build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true - exclude: "lib/*" + exclude: "lib/*,tools/crash-handler/*" cmake_command: >- cmake -S. -Bbuild-clang-tidy -DCMAKE_BUILD_TYPE=Release diff --git a/.gitmodules b/.gitmodules index 571cc0f44..cb1235a85 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,6 +38,6 @@ [submodule "lib/lua/src"] path = lib/lua/src url = https://github.com/lua/lua -[submodule "lib/crashpad"] - path = lib/crashpad - url = https://github.com/getsentry/crashpad +[submodule "tools/crash-handler"] + path = tools/crash-handler + url = https://github.com/Chatterino/crash-handler diff --git a/CHANGELOG.md b/CHANGELOG.md index e21c44a4f..0f9599002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) - Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) - Minor: Add a new completion API for experimental plugins feature. (#5000) +- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e74581b6..f6b8281e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,7 +210,7 @@ if (CHATTERINO_PLUGINS) endif() if (BUILD_WITH_CRASHPAD) - add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL) + add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler") endif() # Used to provide a date of build in the About page (for nightly builds). Getting the actual time of diff --git a/lib/crashpad b/lib/crashpad deleted file mode 160000 index 89991e991..000000000 --- a/lib/crashpad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 89991e9910bc4c0893e45c8cfad0bdd31cc25a5c diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 87deafa8a..424c1d25b 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -44,6 +44,11 @@ public: return nullptr; } + CrashHandler *getCrashHandler() override + { + return nullptr; + } + CommandController *getCommands() override { return nullptr; diff --git a/src/Application.cpp b/src/Application.cpp index d8a7a5754..7cfef3e34 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -38,6 +38,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" #include "singletons/helper/LoggingChannel.hpp" @@ -113,6 +114,7 @@ Application::Application(Settings &_settings, Paths &_paths) , toasts(&this->emplace()) , imageUploader(&this->emplace()) , seventvAPI(&this->emplace()) + , crashHandler(&this->emplace()) , commands(&this->emplace()) , notifications(&this->emplace()) @@ -174,7 +176,9 @@ void Application::initialize(Settings &settings, Paths &paths) singleton->initialize(settings, paths); } - // add crash message + // Show crash message. + // On Windows, the crash message was already shown. +#ifndef Q_OS_WIN if (!getArgs().isFramelessEmbed && getArgs().crashRecovery) { if (auto selected = @@ -195,6 +199,7 @@ void Application::initialize(Settings &settings, Paths &paths) } } } +#endif this->windows->updateWordTypeMask(); diff --git a/src/Application.hpp b/src/Application.hpp index 5dec1e906..75d8bf373 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -44,6 +44,7 @@ class FfzBadges; class SeventvBadges; class ImageUploader; class SeventvAPI; +class CrashHandler; class IApplication { @@ -60,6 +61,7 @@ public: virtual HotkeyController *getHotkeys() = 0; virtual WindowManager *getWindows() = 0; virtual Toasts *getToasts() = 0; + virtual CrashHandler *getCrashHandler() = 0; virtual CommandController *getCommands() = 0; virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; @@ -102,6 +104,7 @@ public: Toasts *const toasts{}; ImageUploader *const imageUploader{}; SeventvAPI *const seventvAPI{}; + CrashHandler *const crashHandler{}; CommandController *const commands{}; NotificationController *const notifications{}; @@ -148,6 +151,10 @@ public: { return this->toasts; } + CrashHandler *getCrashHandler() override + { + return this->crashHandler; + } CommandController *getCommands() override { return this->commands; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53ed53a7b..f16ea09cf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -289,8 +289,6 @@ set(SOURCE_FILES messages/search/SubtierPredicate.cpp messages/search/SubtierPredicate.hpp - providers/Crashpad.cpp - providers/Crashpad.hpp providers/IvrApi.cpp providers/IvrApi.hpp providers/LinkResolver.cpp @@ -425,6 +423,8 @@ set(SOURCE_FILES singletons/Badges.cpp singletons/Badges.hpp + singletons/CrashHandler.cpp + singletons/CrashHandler.hpp singletons/Emotes.cpp singletons/Emotes.hpp singletons/Fonts.cpp @@ -1007,7 +1007,6 @@ endif () if (BUILD_WITH_CRASHPAD) target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD) target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client) - set_target_directory_hierarchy(crashpad_handler crashpad) endif() # Configure compiler warnings diff --git a/src/RunGui.cpp b/src/RunGui.cpp index a495fbef4..00b4b7497 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -5,6 +5,7 @@ #include "common/Modes.hpp" #include "common/NetworkManager.hpp" #include "common/QLogging.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Paths.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" @@ -99,21 +100,10 @@ namespace { void showLastCrashDialog() { - //#ifndef C_DISABLE_CRASH_DIALOG - // LastRunCrashDialog dialog; - - // switch (dialog.exec()) - // { - // case QDialog::Accepted: - // { - // }; - // break; - // default: - // { - // _exit(0); - // } - // } - //#endif + auto *dialog = new LastRunCrashDialog; + // Use exec() over open() to block the app from being loaded + // and to be able to set the safe mode. + dialog->exec(); } void createRunningFile(const QString &path) @@ -131,14 +121,13 @@ namespace { } std::chrono::steady_clock::time_point signalsInitTime; - bool restartOnSignal = false; [[noreturn]] void handleSignal(int signum) { using namespace std::chrono_literals; - if (restartOnSignal && - std::chrono::steady_clock::now() - signalsInitTime > 30s) + if (std::chrono::steady_clock::now() - signalsInitTime > 30s && + getIApp()->getCrashHandler()->shouldRecover()) { QProcess proc; @@ -240,9 +229,12 @@ void runGui(QApplication &a, Paths &paths, Settings &settings) initResources(); initSignalHandler(); - settings.restartOnCrash.connect([](const bool &value) { - restartOnSignal = value; - }); +#ifdef Q_OS_WIN + if (getArgs().crashRecovery) + { + showLastCrashDialog(); + } +#endif auto thread = std::thread([dir = paths.miscDirectory] { { @@ -279,30 +271,11 @@ void runGui(QApplication &a, Paths &paths, Settings &settings) chatterino::NetworkManager::init(); chatterino::Updates::instance().checkForUpdates(); -#ifdef C_USE_BREAKPAD - QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes"); -#endif - - // Running file - auto runningPath = - paths.miscDirectory + "/running_" + paths.applicationFilePathHash; - - if (QFile::exists(runningPath)) - { - showLastCrashDialog(); - } - else - { - createRunningFile(runningPath); - } - Application app(settings, paths); app.initialize(settings, paths); app.run(a); app.save(); - removeRunningFile(runningPath); - if (!getArgs().dontSaveSettings) { pajlada::Settings::SettingManager::gSave(); diff --git a/src/common/Args.cpp b/src/common/Args.cpp index 7bc48573c..1096372cd 100644 --- a/src/common/Args.cpp +++ b/src/common/Args.cpp @@ -1,6 +1,7 @@ #include "Args.hpp" #include "common/QLogging.hpp" +#include "debug/AssertInGuiThread.hpp" #include "singletons/Paths.hpp" #include "singletons/WindowManager.hpp" #include "util/AttachToConsole.hpp" @@ -14,6 +15,55 @@ #include #include +namespace { + +template +QCommandLineOption hiddenOption(Args... args) +{ + QCommandLineOption opt(args...); + opt.setFlags(QCommandLineOption::HiddenFromHelp); + return opt; +} + +QStringList extractCommandLine( + const QCommandLineParser &parser, + std::initializer_list options) +{ + QStringList args; + for (const auto &option : options) + { + if (parser.isSet(option)) + { + auto optionName = option.names().first(); + if (optionName.length() == 1) + { + optionName.prepend(u'-'); + } + else + { + optionName.prepend("--"); + } + + auto values = parser.values(option); + if (values.empty()) + { + args += optionName; + } + else + { + for (const auto &value : values) + { + args += optionName; + args += value; + } + } + } + } + return args; +} + +} // namespace + namespace chatterino { Args::Args(const QApplication &app) @@ -23,39 +73,44 @@ Args::Args(const QApplication &app) parser.addHelpOption(); // Used internally by app to restart after unexpected crashes - QCommandLineOption crashRecoveryOption("crash-recovery"); - crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto crashRecoveryOption = hiddenOption("crash-recovery"); + auto exceptionCodeOption = hiddenOption("cr-exception-code", "", "code"); + auto exceptionMessageOption = + hiddenOption("cr-exception-message", "", "message"); // Added to ignore the parent-window option passed during native messaging - QCommandLineOption parentWindowOption("parent-window"); - parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp); - QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "", - "window-id"); - parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto parentWindowOption = hiddenOption("parent-window"); + auto parentWindowIdOption = + hiddenOption("x-attach-split-to-window", "", "window-id"); // Verbose - QCommandLineOption verboseOption({{"v", "verbose"}, - "Attaches to the Console on windows, " - "allowing you to see debug output."}); - crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto verboseOption = QCommandLineOption( + QStringList{"v", "verbose"}, "Attaches to the Console on windows, " + "allowing you to see debug output."); + // Safe mode QCommandLineOption safeModeOption( "safe-mode", "Starts Chatterino without loading Plugins and always " "show the settings button."); - parser.addOptions({ - {{"V", "version"}, "Displays version information."}, - crashRecoveryOption, - parentWindowOption, - parentWindowIdOption, - verboseOption, - safeModeOption, - }); - parser.addOption(QCommandLineOption( + // Channel layout + auto channelLayout = QCommandLineOption( {"c", "channels"}, "Joins only supplied channels on startup. Use letters with colons to " "specify platform. Only Twitch channels are supported at the moment.\n" "If platform isn't specified, default is Twitch.", - "t:channel1;t:channel2;...")); + "t:channel1;t:channel2;..."); + + parser.addOptions({ + {{"V", "version"}, "Displays version information."}, + crashRecoveryOption, + exceptionCodeOption, + exceptionMessageOption, + parentWindowOption, + parentWindowIdOption, + verboseOption, + safeModeOption, + channelLayout, + }); if (!parser.parse(app.arguments())) { @@ -75,15 +130,25 @@ Args::Args(const QApplication &app) (args.size() > 0 && (args[0].startsWith("chrome-extension://") || args[0].endsWith(".json"))); - if (parser.isSet("c")) + if (parser.isSet(channelLayout)) { - this->applyCustomChannelLayout(parser.value("c")); + this->applyCustomChannelLayout(parser.value(channelLayout)); } this->verbose = parser.isSet(verboseOption); this->printVersion = parser.isSet("V"); - this->crashRecovery = parser.isSet("crash-recovery"); + + this->crashRecovery = parser.isSet(crashRecoveryOption); + if (parser.isSet(exceptionCodeOption)) + { + this->exceptionCode = + static_cast(parser.value(exceptionCodeOption).toULong()); + } + if (parser.isSet(exceptionMessageOption)) + { + this->exceptionMessage = parser.value(exceptionMessageOption); + } if (parser.isSet(parentWindowIdOption)) { @@ -97,6 +162,17 @@ Args::Args(const QApplication &app) { this->safeMode = true; } + + this->currentArguments_ = extractCommandLine(parser, { + verboseOption, + safeModeOption, + channelLayout, + }); +} + +QStringList Args::currentArguments() const +{ + return this->currentArguments_; } void Args::applyCustomChannelLayout(const QString &argValue) diff --git a/src/common/Args.hpp b/src/common/Args.hpp index 73144b7da..39b7d627b 100644 --- a/src/common/Args.hpp +++ b/src/common/Args.hpp @@ -9,13 +9,37 @@ namespace chatterino { /// Command line arguments passed to Chatterino. +/// +/// All accepted arguments: +/// +/// Crash recovery: +/// --crash-recovery +/// --cr-exception-code code +/// --cr-exception-message message +/// +/// Native messaging: +/// --parent-window +/// --x-attach-split-to-window=window-id +/// +/// -v, --verbose +/// -V, --version +/// -c, --channels=t:channel1;t:channel2;... +/// --safe-mode +/// +/// See documentation on `QGuiApplication` for documentation on Qt arguments like -platform. class Args { public: Args(const QApplication &app); bool printVersion{}; + bool crashRecovery{}; + /// Native, platform-specific exception code from crashpad + std::optional exceptionCode{}; + /// Text version of the exception code. Potentially contains more context. + std::optional exceptionMessage{}; + bool shouldRunBrowserExtensionHost{}; // Shows a single chat. Used on windows to embed in another application. bool isFramelessEmbed{}; @@ -28,8 +52,12 @@ public: bool verbose{}; bool safeMode{}; + QStringList currentArguments() const; + private: void applyCustomChannelLayout(const QString &argValue); + + QStringList currentArguments_; }; void initArgs(const QApplication &app); diff --git a/src/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp index 07d672751..f83b820a1 100644 --- a/src/common/FlagsEnum.hpp +++ b/src/common/FlagsEnum.hpp @@ -97,6 +97,11 @@ public: return !this->hasAny(flags); } + T value() const + { + return this->value_; + } + private: T value_{}; }; diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 31c035d85..de4ef056c 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -12,6 +12,8 @@ Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold); Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold); Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold); Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold); +Q_LOGGING_CATEGORY(chatterinoCrashhandler, "chatterino.crashhandler", + logThreshold); Q_LOGGING_CATEGORY(chatterinoEmoji, "chatterino.emoji", logThreshold); Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold); Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index 01500f1da..36daa0e1e 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -8,6 +8,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark); Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv); Q_DECLARE_LOGGING_CATEGORY(chatterinoCache); Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon); +Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler); Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji); Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv); Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes); diff --git a/src/main.cpp b/src/main.cpp index 9dce310a0..1366ae5cb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,11 +4,11 @@ #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" -#include "providers/Crashpad.hpp" #include "providers/IvrApi.hpp" #include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/api/Helix.hpp" #include "RunGui.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/AttachToConsole.hpp" diff --git a/src/providers/Crashpad.cpp b/src/providers/Crashpad.cpp deleted file mode 100644 index f81cbe071..000000000 --- a/src/providers/Crashpad.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#ifdef CHATTERINO_WITH_CRASHPAD -# include "providers/Crashpad.hpp" - -# include "common/QLogging.hpp" -# include "singletons/Paths.hpp" - -# include -# include -# include - -# include -# include - -namespace { - -/// The name of the crashpad handler executable. -/// This varies across platforms -# if defined(Q_OS_UNIX) -const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler"); -# elif defined(Q_OS_WINDOWS) -const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler.exe"); -# else -# error Unsupported platform -# endif - -/// Converts a QString into the platform string representation. -# if defined(Q_OS_UNIX) -std::string nativeString(const QString &s) -{ - return s.toStdString(); -} -# elif defined(Q_OS_WINDOWS) -std::wstring nativeString(const QString &s) -{ - return s.toStdWString(); -} -# else -# error Unsupported platform -# endif - -} // namespace - -namespace chatterino { - -std::unique_ptr installCrashHandler() -{ - // Currently, the following directory layout is assumed: - // [applicationDirPath] - // │ - // ├─chatterino - // │ - // ╰─[crashpad] - // │ - // ╰─crashpad_handler - // TODO: The location of the binary might vary across platforms - auto crashpadBinDir = QDir(QApplication::applicationDirPath()); - - if (!crashpadBinDir.cd("crashpad")) - { - qCDebug(chatterinoApp) << "Cannot find crashpad directory"; - return nullptr; - } - if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME)) - { - qCDebug(chatterinoApp) << "Cannot find crashpad handler executable"; - return nullptr; - } - - const auto handlerPath = base::FilePath(nativeString( - crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME))); - - // Argument passed in --database - // > Crash reports are written to this database, and if uploads are enabled, - // uploaded from this database to a crash report collection server. - const auto databaseDir = - base::FilePath(nativeString(getPaths()->crashdumpDirectory)); - - auto client = std::make_unique(); - - // See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md - // for documentation on available options. - if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, {}, - true, false)) - { - qCDebug(chatterinoApp) << "Failed to start crashpad handler"; - return nullptr; - } - - qCDebug(chatterinoApp) << "Started crashpad handler"; - return client; -} - -} // namespace chatterino - -#endif diff --git a/src/providers/Crashpad.hpp b/src/providers/Crashpad.hpp deleted file mode 100644 index d15f3fcb7..000000000 --- a/src/providers/Crashpad.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#ifdef CHATTERINO_WITH_CRASHPAD -# include - -# include - -namespace chatterino { - -std::unique_ptr installCrashHandler(); - -} // namespace chatterino - -#endif diff --git a/src/singletons/CrashHandler.cpp b/src/singletons/CrashHandler.cpp new file mode 100644 index 000000000..76518e909 --- /dev/null +++ b/src/singletons/CrashHandler.cpp @@ -0,0 +1,230 @@ +#include "singletons/CrashHandler.hpp" + +#include "common/Args.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef CHATTERINO_WITH_CRASHPAD +# include + +# include +# include +#endif + +namespace { + +using namespace chatterino; +using namespace literals; + +/// The name of the crashpad handler executable. +/// This varies across platforms +#if defined(Q_OS_UNIX) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler"); +#elif defined(Q_OS_WINDOWS) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler.exe"); +#else +# error Unsupported platform +#endif + +/// Converts a QString into the platform string representation. +#if defined(Q_OS_UNIX) +std::string nativeString(const QString &s) +{ + return s.toStdString(); +} +#elif defined(Q_OS_WINDOWS) +std::wstring nativeString(const QString &s) +{ + return s.toStdWString(); +} +#else +# error Unsupported platform +#endif + +const QString RECOVERY_FILE = u"chatterino-recovery.json"_s; + +/// The recovery options are saved outside the settings +/// to be able to read them without loading the settings. +/// +/// The flags are saved in the `RECOVERY_FILE` as JSON. +std::optional readRecoverySettings(const Paths &paths) +{ + QFile file(QDir(paths.crashdumpDirectory).filePath(RECOVERY_FILE)); + if (!file.open(QFile::ReadOnly)) + { + return std::nullopt; + } + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCWarning(chatterinoCrashhandler) + << "Failed to parse recovery settings" << error.errorString(); + return std::nullopt; + } + + const auto obj = doc.object(); + auto shouldRecover = obj["shouldRecover"_L1]; + if (!shouldRecover.isBool()) + { + return std::nullopt; + } + + return shouldRecover.toBool(); +} + +bool canRestart(const Paths &paths) +{ +#ifdef NDEBUG + const auto &args = chatterino::getArgs(); + if (args.isFramelessEmbed || args.shouldRunBrowserExtensionHost) + { + return false; + } + + auto settings = readRecoverySettings(paths); + if (!settings) + { + return false; // default, no settings found + } + return *settings; +#else + (void)paths; + return false; +#endif +} + +/// This encodes the arguments into a single string. +/// +/// The command line arguments are joined by '+'. A plus is escaped by an +/// additional plus ('++' -> '+'). +/// +/// The decoding happens in crash-handler/src/CommandLine.cpp +std::string encodeArguments() +{ + std::string args; + for (auto arg : getArgs().currentArguments()) + { + if (!args.empty()) + { + args.push_back('+'); + } + args += arg.replace(u'+', u"++"_s).toStdString(); + } + return args; +} + +} // namespace + +namespace chatterino { + +using namespace std::string_literals; + +void CrashHandler::initialize(Settings & /*settings*/, Paths &paths) +{ + auto optSettings = readRecoverySettings(paths); + if (optSettings) + { + this->shouldRecover_ = *optSettings; + } + else + { + // By default, we don't restart after a crash. + this->saveShouldRecover(false); + } +} + +void CrashHandler::saveShouldRecover(bool value) +{ + this->shouldRecover_ = value; + + QFile file(QDir(getPaths()->crashdumpDirectory).filePath(RECOVERY_FILE)); + if (!file.open(QFile::WriteOnly | QFile::Truncate)) + { + qCWarning(chatterinoCrashhandler) + << "Failed to open" << file.fileName(); + return; + } + file.write(QJsonDocument(QJsonObject{ + {"shouldRecover"_L1, value}, + }) + .toJson(QJsonDocument::Compact)); +} + +#ifdef CHATTERINO_WITH_CRASHPAD +std::unique_ptr installCrashHandler() +{ + // Currently, the following directory layout is assumed: + // [applicationDirPath] + // ├─chatterino(.exe) + // ╰─[crashpad] + // ╰─crashpad-handler(.exe) + // TODO: The location of the binary might vary across platforms + auto crashpadBinDir = QDir(QApplication::applicationDirPath()); + + if (!crashpadBinDir.cd("crashpad")) + { + qCDebug(chatterinoCrashhandler) << "Cannot find crashpad directory"; + return nullptr; + } + if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME)) + { + qCDebug(chatterinoCrashhandler) + << "Cannot find crashpad handler executable"; + return nullptr; + } + + auto handlerPath = base::FilePath(nativeString( + crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME))); + + // Argument passed in --database + // > Crash reports are written to this database, and if uploads are enabled, + // uploaded from this database to a crash report collection server. + auto databaseDir = + base::FilePath(nativeString(getPaths()->crashdumpDirectory)); + + auto client = std::make_unique(); + + std::map annotations{ + { + "canRestart"s, + canRestart(*getPaths()) ? "true"s : "false"s, + }, + { + "exePath"s, + QApplication::applicationFilePath().toStdString(), + }, + { + "startedAt"s, + QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toStdString(), + }, + { + "exeArguments"s, + encodeArguments(), + }, + }; + + // See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md + // for documentation on available options. + if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, annotations, + {}, true, false)) + { + qCDebug(chatterinoCrashhandler) << "Failed to start crashpad handler"; + return nullptr; + } + + qCDebug(chatterinoCrashhandler) << "Started crashpad handler"; + return client; +} +#endif + +} // namespace chatterino diff --git a/src/singletons/CrashHandler.hpp b/src/singletons/CrashHandler.hpp new file mode 100644 index 000000000..7f47708a0 --- /dev/null +++ b/src/singletons/CrashHandler.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "common/Singleton.hpp" + +#include + +#ifdef CHATTERINO_WITH_CRASHPAD +# include + +# include +#endif + +namespace chatterino { + +class CrashHandler : public Singleton +{ +public: + bool shouldRecover() const + { + return this->shouldRecover_; + } + + /// Sets and saves whether Chatterino should restart on a crash + void saveShouldRecover(bool value); + + void initialize(Settings &settings, Paths &paths) override; + +private: + bool shouldRecover_ = false; +}; + +#ifdef CHATTERINO_WITH_CRASHPAD +std::unique_ptr installCrashHandler(); +#endif + +} // namespace chatterino diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 8e650de87..612f8ebef 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -510,7 +510,6 @@ public: ThumbnailPreviewMode::AlwaysShow, }; QStringSetting cachePath = {"/cache/path", ""}; - BoolSetting restartOnCrash = {"/misc/restartOnCrash", false}; BoolSetting attachExtensionToAnyProcess = { "/misc/attachExtensionToAnyProcess", false}; BoolSetting askOnImageUpload = {"/misc/askOnImageUpload", true}; diff --git a/src/widgets/dialogs/LastRunCrashDialog.cpp b/src/widgets/dialogs/LastRunCrashDialog.cpp index f3dc77921..f038b03f6 100644 --- a/src/widgets/dialogs/LastRunCrashDialog.cpp +++ b/src/widgets/dialogs/LastRunCrashDialog.cpp @@ -1,93 +1,109 @@ -#include "LastRunCrashDialog.hpp" +#include "widgets/dialogs/LastRunCrashDialog.hpp" -#include "singletons/Updates.hpp" +#include "common/Args.hpp" +#include "common/Literals.hpp" +#include "common/Modes.hpp" +#include "singletons/Paths.hpp" #include "util/LayoutCreator.hpp" -#include "util/PostToThread.hpp" +#include #include +#include #include +#include +#include #include +#include +#include #include +namespace { + +using namespace chatterino::literals; + +const std::initializer_list MESSAGES = { + u"Oops..."_s, u"NotLikeThis"_s, + u"NOOOOOO"_s, u"I'm sorry"_s, + u"We're sorry"_s, u"My bad"_s, + u"FailFish"_s, u"O_o"_s, + u"Sorry :("_s, u"I blame cosmic rays"_s, + u"I blame TMI"_s, u"I blame Helix"_s, + u"Oopsie woopsie"_s, +}; + +QString randomMessage() +{ + return *(MESSAGES.begin() + + (QRandomGenerator::global()->generate64() % MESSAGES.size())); +} + +} // namespace + namespace chatterino { +using namespace literals; + LastRunCrashDialog::LastRunCrashDialog() { this->setWindowFlag(Qt::WindowContextHelpButtonHint, false); - this->setWindowTitle("Chatterino"); + this->setWindowTitle(u"Chatterino - " % randomMessage()); auto layout = LayoutCreator(this).setLayoutType(); - layout.emplace("The application wasn't terminated properly the " - "last time it was executed."); + QString text = + u"Chatterino unexpectedly crashed and restarted. "_s + "You can disable automatic restarts in the settings.

"; + +#ifdef CHATTERINO_WITH_CRASHPAD + auto reportsDir = + QDir(getPaths()->crashdumpDirectory).filePath(u"reports"_s); + text += u"A crash report has been saved to " + "" % reportsDir % u".
"; + + if (getArgs().exceptionCode) + { + text += u"The last run crashed with code 0x" % + QString::number(*getArgs().exceptionCode, 16) % u""; + + if (getArgs().exceptionMessage) + { + text += u" (" % *getArgs().exceptionMessage % u")"; + } + + text += u".
"_s; + } + + text += + "Crash reports are only stored locally and never uploaded.
" + "
Please report " + "the crash " + u"so it can be prevented in the future."_s; + + if (Modes::instance().isNightly) + { + text += u" Make sure you're using the latest nightly version!"_s; + } + + text += + u"
For more information, consult the wiki."_s; +#endif + + auto label = layout.emplace(text); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + label->setOpenExternalLinks(true); + label->setWordWrap(true); layout->addSpacing(16); - // auto update = layout.emplace(); auto buttons = layout.emplace(); - // auto *installUpdateButton = buttons->addButton("Install Update", - // QDialogButtonBox::NoRole); installUpdateButton->setEnabled(false); - // QObject::connect(installUpdateButton, &QPushButton::clicked, [this, - // update]() mutable { - // auto &updateManager = UpdateManager::instance(); - - // updateManager.installUpdates(); - // this->setEnabled(false); - // update->setText("Downloading updates..."); - // }); - - auto *okButton = - buttons->addButton("Ignore", QDialogButtonBox::ButtonRole::NoRole); + auto *okButton = buttons->addButton(u"Ok"_s, QDialogButtonBox::AcceptRole); QObject::connect(okButton, &QPushButton::clicked, [this] { this->accept(); }); - - // Updates - // auto updateUpdateLabel = [update]() mutable { - // auto &updateManager = UpdateManager::instance(); - - // switch (updateManager.getStatus()) { - // case UpdateManager::None: { - // update->setText("Not checking for updates."); - // } break; - // case UpdateManager::Searching: { - // update->setText("Checking for updates..."); - // } break; - // case UpdateManager::UpdateAvailable: { - // update->setText("Update available."); - // } break; - // case UpdateManager::NoUpdateAvailable: { - // update->setText("No update abailable."); - // } break; - // case UpdateManager::SearchFailed: { - // update->setText("Error while searching for update.\nEither - // the update service is " - // "temporarily down or there is an issue - // with your installation."); - // } break; - // case UpdateManager::Downloading: { - // update->setText( - // "Downloading the update. Chatterino will close once - // the download is done."); - // } break; - // case UpdateManager::DownloadFailed: { - // update->setText("Download failed."); - // } break; - // case UpdateManager::WriteFileFailed: { - // update->setText("Writing the update file to the hard drive - // failed."); - // } break; - // } - // }; - - // updateUpdateLabel(); - // this->signalHolder_.managedConnect(updateManager.statusUpdated, - // [updateUpdateLabel](auto) mutable { - // postToThread([updateUpdateLabel]() mutable { updateUpdateLabel(); - // }); - // }); } } // namespace chatterino diff --git a/src/widgets/dialogs/LastRunCrashDialog.hpp b/src/widgets/dialogs/LastRunCrashDialog.hpp index ebd1e3a4c..a4aa75ef7 100644 --- a/src/widgets/dialogs/LastRunCrashDialog.hpp +++ b/src/widgets/dialogs/LastRunCrashDialog.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include namespace chatterino { @@ -9,9 +8,6 @@ class LastRunCrashDialog : public QDialog { public: LastRunCrashDialog(); - -private: - pajlada::Signals::SignalHolder signalHolder_; }; } // namespace chatterino diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 338219976..8498b904c 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -8,6 +8,7 @@ #include "controllers/sound/ISoundController.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Fonts.hpp" #include "singletons/NativeMessaging.hpp" #include "singletons/Paths.hpp" @@ -875,8 +876,14 @@ void GeneralPage::initLayout(GeneralPageView &layout) s.openLinksIncognito); } - layout.addCheckbox( - "Restart on crash", s.restartOnCrash, false, + layout.addCustomCheckbox( + "Restart on crash (requires restart)", + [] { + return getApp()->crashHandler->shouldRecover(); + }, + [](bool on) { + return getApp()->crashHandler->saveShouldRecover(on); + }, "When possible, restart Chatterino if the program crashes"); #if defined(Q_OS_LINUX) && !defined(NO_QTKEYCHAIN) diff --git a/src/widgets/settingspages/GeneralPageView.cpp b/src/widgets/settingspages/GeneralPageView.cpp index b063c6177..95a0d42fc 100644 --- a/src/widgets/settingspages/GeneralPageView.cpp +++ b/src/widgets/settingspages/GeneralPageView.cpp @@ -125,6 +125,28 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text, return check; } +QCheckBox *GeneralPageView::addCustomCheckbox(const QString &text, + const std::function &load, + std::function save, + const QString &toolTipText) +{ + auto *check = new QCheckBox(text); + this->addToolTip(*check, toolTipText); + + check->setChecked(load()); + + QObject::connect(check, &QCheckBox::toggled, this, + [save = std::move(save)](bool state) { + save(state); + }); + + this->addWidget(check); + + this->groups_.back().widgets.push_back({check, {text}}); + + return check; +} + ComboBox *GeneralPageView::addDropdown(const QString &text, const QStringList &list, QString toolTipText) diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index 61a77fbde..382680dad 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -103,6 +103,11 @@ public: /// @param inverse Inverses true to false and vice versa QCheckBox *addCheckbox(const QString &text, BoolSetting &setting, bool inverse = false, QString toolTipText = {}); + QCheckBox *addCustomCheckbox(const QString &text, + const std::function &load, + std::function save, + const QString &toolTipText = {}); + ComboBox *addDropdown(const QString &text, const QStringList &items, QString toolTipText = {}); ComboBox *addDropdown(const QString &text, const QStringList &items, diff --git a/tools/crash-handler b/tools/crash-handler new file mode 160000 index 000000000..9753fe802 --- /dev/null +++ b/tools/crash-handler @@ -0,0 +1 @@ +Subproject commit 9753fe802710b2df00f2287ec2e1ca78c251d085