feat: Add crash recovery on Windows (#5012)

This commit is contained in:
nerix 2023-12-24 15:38:58 +01:00 committed by GitHub
parent 2cb965d352
commit 25add89b14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 563 additions and 258 deletions

View file

@ -29,6 +29,7 @@ Checks: "-*,
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-cert-err58-cpp, -cert-err58-cpp,
-modernize-avoid-c-arrays
" "
CheckOptions: CheckOptions:
- key: readability-identifier-naming.ClassCase - key: readability-identifier-naming.ClassCase

View file

@ -230,9 +230,9 @@ jobs:
run: | run: |
cd build cd build
set cl=/MP set cl=/MP
nmake /S /NOLOGO crashpad_handler nmake /S /NOLOGO chatterino-crash-handler
mkdir Chatterino2/crashpad 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 7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb
- name: Prepare build dir (windows) - name: Prepare build dir (windows)

View file

@ -124,7 +124,7 @@ jobs:
build_dir: build-clang-tidy build_dir: build-clang-tidy
config_file: ".clang-tidy" config_file: ".clang-tidy"
split_workflow: true split_workflow: true
exclude: "lib/*" exclude: "lib/*,tools/crash-handler/*"
cmake_command: >- cmake_command: >-
cmake -S. -Bbuild-clang-tidy cmake -S. -Bbuild-clang-tidy
-DCMAKE_BUILD_TYPE=Release -DCMAKE_BUILD_TYPE=Release

6
.gitmodules vendored
View file

@ -38,6 +38,6 @@
[submodule "lib/lua/src"] [submodule "lib/lua/src"]
path = lib/lua/src path = lib/lua/src
url = https://github.com/lua/lua url = https://github.com/lua/lua
[submodule "lib/crashpad"] [submodule "tools/crash-handler"]
path = lib/crashpad path = tools/crash-handler
url = https://github.com/getsentry/crashpad url = https://github.com/Chatterino/crash-handler

View file

@ -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: 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: 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: 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 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: 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) - 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)

View file

@ -210,7 +210,7 @@ if (CHATTERINO_PLUGINS)
endif() endif()
if (BUILD_WITH_CRASHPAD) if (BUILD_WITH_CRASHPAD)
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL) add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
endif() endif()
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of # Used to provide a date of build in the About page (for nightly builds). Getting the actual time of

@ -1 +0,0 @@
Subproject commit 89991e9910bc4c0893e45c8cfad0bdd31cc25a5c

View file

@ -44,6 +44,11 @@ public:
return nullptr; return nullptr;
} }
CrashHandler *getCrashHandler() override
{
return nullptr;
}
CommandController *getCommands() override CommandController *getCommands() override
{ {
return nullptr; return nullptr;

View file

@ -38,6 +38,7 @@
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "singletons/helper/LoggingChannel.hpp" #include "singletons/helper/LoggingChannel.hpp"
@ -113,6 +114,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, toasts(&this->emplace<Toasts>()) , toasts(&this->emplace<Toasts>())
, imageUploader(&this->emplace<ImageUploader>()) , imageUploader(&this->emplace<ImageUploader>())
, seventvAPI(&this->emplace<SeventvAPI>()) , seventvAPI(&this->emplace<SeventvAPI>())
, crashHandler(&this->emplace<CrashHandler>())
, commands(&this->emplace<CommandController>()) , commands(&this->emplace<CommandController>())
, notifications(&this->emplace<NotificationController>()) , notifications(&this->emplace<NotificationController>())
@ -174,7 +176,9 @@ void Application::initialize(Settings &settings, Paths &paths)
singleton->initialize(settings, 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 (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
{ {
if (auto selected = if (auto selected =
@ -195,6 +199,7 @@ void Application::initialize(Settings &settings, Paths &paths)
} }
} }
} }
#endif
this->windows->updateWordTypeMask(); this->windows->updateWordTypeMask();

View file

@ -44,6 +44,7 @@ class FfzBadges;
class SeventvBadges; class SeventvBadges;
class ImageUploader; class ImageUploader;
class SeventvAPI; class SeventvAPI;
class CrashHandler;
class IApplication class IApplication
{ {
@ -60,6 +61,7 @@ public:
virtual HotkeyController *getHotkeys() = 0; virtual HotkeyController *getHotkeys() = 0;
virtual WindowManager *getWindows() = 0; virtual WindowManager *getWindows() = 0;
virtual Toasts *getToasts() = 0; virtual Toasts *getToasts() = 0;
virtual CrashHandler *getCrashHandler() = 0;
virtual CommandController *getCommands() = 0; virtual CommandController *getCommands() = 0;
virtual HighlightController *getHighlights() = 0; virtual HighlightController *getHighlights() = 0;
virtual NotificationController *getNotifications() = 0; virtual NotificationController *getNotifications() = 0;
@ -102,6 +104,7 @@ public:
Toasts *const toasts{}; Toasts *const toasts{};
ImageUploader *const imageUploader{}; ImageUploader *const imageUploader{};
SeventvAPI *const seventvAPI{}; SeventvAPI *const seventvAPI{};
CrashHandler *const crashHandler{};
CommandController *const commands{}; CommandController *const commands{};
NotificationController *const notifications{}; NotificationController *const notifications{};
@ -148,6 +151,10 @@ public:
{ {
return this->toasts; return this->toasts;
} }
CrashHandler *getCrashHandler() override
{
return this->crashHandler;
}
CommandController *getCommands() override CommandController *getCommands() override
{ {
return this->commands; return this->commands;

View file

@ -289,8 +289,6 @@ set(SOURCE_FILES
messages/search/SubtierPredicate.cpp messages/search/SubtierPredicate.cpp
messages/search/SubtierPredicate.hpp messages/search/SubtierPredicate.hpp
providers/Crashpad.cpp
providers/Crashpad.hpp
providers/IvrApi.cpp providers/IvrApi.cpp
providers/IvrApi.hpp providers/IvrApi.hpp
providers/LinkResolver.cpp providers/LinkResolver.cpp
@ -425,6 +423,8 @@ set(SOURCE_FILES
singletons/Badges.cpp singletons/Badges.cpp
singletons/Badges.hpp singletons/Badges.hpp
singletons/CrashHandler.cpp
singletons/CrashHandler.hpp
singletons/Emotes.cpp singletons/Emotes.cpp
singletons/Emotes.hpp singletons/Emotes.hpp
singletons/Fonts.cpp singletons/Fonts.cpp
@ -1007,7 +1007,6 @@ endif ()
if (BUILD_WITH_CRASHPAD) if (BUILD_WITH_CRASHPAD)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD) target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD)
target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client) target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client)
set_target_directory_hierarchy(crashpad_handler crashpad)
endif() endif()
# Configure compiler warnings # Configure compiler warnings

View file

@ -5,6 +5,7 @@
#include "common/Modes.hpp" #include "common/Modes.hpp"
#include "common/NetworkManager.hpp" #include "common/NetworkManager.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -99,21 +100,10 @@ namespace {
void showLastCrashDialog() void showLastCrashDialog()
{ {
//#ifndef C_DISABLE_CRASH_DIALOG auto *dialog = new LastRunCrashDialog;
// LastRunCrashDialog dialog; // Use exec() over open() to block the app from being loaded
// and to be able to set the safe mode.
// switch (dialog.exec()) dialog->exec();
// {
// case QDialog::Accepted:
// {
// };
// break;
// default:
// {
// _exit(0);
// }
// }
//#endif
} }
void createRunningFile(const QString &path) void createRunningFile(const QString &path)
@ -131,14 +121,13 @@ namespace {
} }
std::chrono::steady_clock::time_point signalsInitTime; std::chrono::steady_clock::time_point signalsInitTime;
bool restartOnSignal = false;
[[noreturn]] void handleSignal(int signum) [[noreturn]] void handleSignal(int signum)
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
if (restartOnSignal && if (std::chrono::steady_clock::now() - signalsInitTime > 30s &&
std::chrono::steady_clock::now() - signalsInitTime > 30s) getIApp()->getCrashHandler()->shouldRecover())
{ {
QProcess proc; QProcess proc;
@ -240,9 +229,12 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
initResources(); initResources();
initSignalHandler(); initSignalHandler();
settings.restartOnCrash.connect([](const bool &value) { #ifdef Q_OS_WIN
restartOnSignal = value; if (getArgs().crashRecovery)
}); {
showLastCrashDialog();
}
#endif
auto thread = std::thread([dir = paths.miscDirectory] { auto thread = std::thread([dir = paths.miscDirectory] {
{ {
@ -279,30 +271,11 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
chatterino::NetworkManager::init(); chatterino::NetworkManager::init();
chatterino::Updates::instance().checkForUpdates(); 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); Application app(settings, paths);
app.initialize(settings, paths); app.initialize(settings, paths);
app.run(a); app.run(a);
app.save(); app.save();
removeRunningFile(runningPath);
if (!getArgs().dontSaveSettings) if (!getArgs().dontSaveSettings)
{ {
pajlada::Settings::SettingManager::gSave(); pajlada::Settings::SettingManager::gSave();

View file

@ -1,6 +1,7 @@
#include "Args.hpp" #include "Args.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/AttachToConsole.hpp" #include "util/AttachToConsole.hpp"
@ -14,6 +15,55 @@
#include <QStringList> #include <QStringList>
#include <QUuid> #include <QUuid>
namespace {
template <class... Args>
QCommandLineOption hiddenOption(Args... args)
{
QCommandLineOption opt(args...);
opt.setFlags(QCommandLineOption::HiddenFromHelp);
return opt;
}
QStringList extractCommandLine(
const QCommandLineParser &parser,
std::initializer_list<QCommandLineOption> 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 { namespace chatterino {
Args::Args(const QApplication &app) Args::Args(const QApplication &app)
@ -23,39 +73,44 @@ Args::Args(const QApplication &app)
parser.addHelpOption(); parser.addHelpOption();
// Used internally by app to restart after unexpected crashes // Used internally by app to restart after unexpected crashes
QCommandLineOption crashRecoveryOption("crash-recovery"); auto crashRecoveryOption = hiddenOption("crash-recovery");
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); 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 // Added to ignore the parent-window option passed during native messaging
QCommandLineOption parentWindowOption("parent-window"); auto parentWindowOption = hiddenOption("parent-window");
parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp); auto parentWindowIdOption =
QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "", hiddenOption("x-attach-split-to-window", "", "window-id");
"window-id");
parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp);
// Verbose // Verbose
QCommandLineOption verboseOption({{"v", "verbose"}, auto verboseOption = QCommandLineOption(
"Attaches to the Console on windows, " QStringList{"v", "verbose"}, "Attaches to the Console on windows, "
"allowing you to see debug output."}); "allowing you to see debug output.");
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); // Safe mode
QCommandLineOption safeModeOption( QCommandLineOption safeModeOption(
"safe-mode", "Starts Chatterino without loading Plugins and always " "safe-mode", "Starts Chatterino without loading Plugins and always "
"show the settings button."); "show the settings button.");
parser.addOptions({ // Channel layout
{{"V", "version"}, "Displays version information."}, auto channelLayout = QCommandLineOption(
crashRecoveryOption,
parentWindowOption,
parentWindowIdOption,
verboseOption,
safeModeOption,
});
parser.addOption(QCommandLineOption(
{"c", "channels"}, {"c", "channels"},
"Joins only supplied channels on startup. Use letters with colons to " "Joins only supplied channels on startup. Use letters with colons to "
"specify platform. Only Twitch channels are supported at the moment.\n" "specify platform. Only Twitch channels are supported at the moment.\n"
"If platform isn't specified, default is Twitch.", "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())) if (!parser.parse(app.arguments()))
{ {
@ -75,15 +130,25 @@ Args::Args(const QApplication &app)
(args.size() > 0 && (args[0].startsWith("chrome-extension://") || (args.size() > 0 && (args[0].startsWith("chrome-extension://") ||
args[0].endsWith(".json"))); 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->verbose = parser.isSet(verboseOption);
this->printVersion = parser.isSet("V"); this->printVersion = parser.isSet("V");
this->crashRecovery = parser.isSet("crash-recovery");
this->crashRecovery = parser.isSet(crashRecoveryOption);
if (parser.isSet(exceptionCodeOption))
{
this->exceptionCode =
static_cast<uint32_t>(parser.value(exceptionCodeOption).toULong());
}
if (parser.isSet(exceptionMessageOption))
{
this->exceptionMessage = parser.value(exceptionMessageOption);
}
if (parser.isSet(parentWindowIdOption)) if (parser.isSet(parentWindowIdOption))
{ {
@ -97,6 +162,17 @@ Args::Args(const QApplication &app)
{ {
this->safeMode = true; this->safeMode = true;
} }
this->currentArguments_ = extractCommandLine(parser, {
verboseOption,
safeModeOption,
channelLayout,
});
}
QStringList Args::currentArguments() const
{
return this->currentArguments_;
} }
void Args::applyCustomChannelLayout(const QString &argValue) void Args::applyCustomChannelLayout(const QString &argValue)

View file

@ -9,13 +9,37 @@
namespace chatterino { namespace chatterino {
/// Command line arguments passed to 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 class Args
{ {
public: public:
Args(const QApplication &app); Args(const QApplication &app);
bool printVersion{}; bool printVersion{};
bool crashRecovery{}; bool crashRecovery{};
/// Native, platform-specific exception code from crashpad
std::optional<uint32_t> exceptionCode{};
/// Text version of the exception code. Potentially contains more context.
std::optional<QString> exceptionMessage{};
bool shouldRunBrowserExtensionHost{}; bool shouldRunBrowserExtensionHost{};
// Shows a single chat. Used on windows to embed in another application. // Shows a single chat. Used on windows to embed in another application.
bool isFramelessEmbed{}; bool isFramelessEmbed{};
@ -28,8 +52,12 @@ public:
bool verbose{}; bool verbose{};
bool safeMode{}; bool safeMode{};
QStringList currentArguments() const;
private: private:
void applyCustomChannelLayout(const QString &argValue); void applyCustomChannelLayout(const QString &argValue);
QStringList currentArguments_;
}; };
void initArgs(const QApplication &app); void initArgs(const QApplication &app);

View file

@ -97,6 +97,11 @@ public:
return !this->hasAny(flags); return !this->hasAny(flags);
} }
T value() const
{
return this->value_;
}
private: private:
T value_{}; T value_{};
}; };

View file

@ -12,6 +12,8 @@ Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold);
Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold); Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold);
Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold); Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold);
Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", 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(chatterinoEmoji, "chatterino.emoji", logThreshold);
Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold); Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold);
Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold); Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold);

View file

@ -8,6 +8,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark);
Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv); Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv);
Q_DECLARE_LOGGING_CATEGORY(chatterinoCache); Q_DECLARE_LOGGING_CATEGORY(chatterinoCache);
Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon); Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon);
Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler);
Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji); Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji);
Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv); Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv);
Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes); Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes);

View file

@ -4,11 +4,11 @@
#include "common/Modes.hpp" #include "common/Modes.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "common/Version.hpp" #include "common/Version.hpp"
#include "providers/Crashpad.hpp"
#include "providers/IvrApi.hpp" #include "providers/IvrApi.hpp"
#include "providers/NetworkConfigurationProvider.hpp" #include "providers/NetworkConfigurationProvider.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "RunGui.hpp" #include "RunGui.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "util/AttachToConsole.hpp" #include "util/AttachToConsole.hpp"

View file

@ -1,95 +0,0 @@
#ifdef CHATTERINO_WITH_CRASHPAD
# include "providers/Crashpad.hpp"
# include "common/QLogging.hpp"
# include "singletons/Paths.hpp"
# include <QApplication>
# include <QDir>
# include <QString>
# include <memory>
# include <string>
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<crashpad::CrashpadClient> 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<crashpad::CrashpadClient>();
// 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

View file

@ -1,14 +0,0 @@
#pragma once
#ifdef CHATTERINO_WITH_CRASHPAD
# include <client/crashpad_client.h>
# include <memory>
namespace chatterino {
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler();
} // namespace chatterino
#endif

View file

@ -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 <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QString>
#ifdef CHATTERINO_WITH_CRASHPAD
# include <QApplication>
# include <memory>
# include <string>
#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<bool> 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<crashpad::CrashpadClient> 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<crashpad::CrashpadClient>();
std::map<std::string, std::string> 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

View file

@ -0,0 +1,36 @@
#pragma once
#include "common/Singleton.hpp"
#include <QtGlobal>
#ifdef CHATTERINO_WITH_CRASHPAD
# include <client/crashpad_client.h>
# include <memory>
#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<crashpad::CrashpadClient> installCrashHandler();
#endif
} // namespace chatterino

View file

@ -510,7 +510,6 @@ public:
ThumbnailPreviewMode::AlwaysShow, ThumbnailPreviewMode::AlwaysShow,
}; };
QStringSetting cachePath = {"/cache/path", ""}; QStringSetting cachePath = {"/cache/path", ""};
BoolSetting restartOnCrash = {"/misc/restartOnCrash", false};
BoolSetting attachExtensionToAnyProcess = { BoolSetting attachExtensionToAnyProcess = {
"/misc/attachExtensionToAnyProcess", false}; "/misc/attachExtensionToAnyProcess", false};
BoolSetting askOnImageUpload = {"/misc/askOnImageUpload", true}; BoolSetting askOnImageUpload = {"/misc/askOnImageUpload", true};

View file

@ -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/LayoutCreator.hpp"
#include "util/PostToThread.hpp"
#include <QDesktopServices>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QDir>
#include <QLabel> #include <QLabel>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton> #include <QPushButton>
#include <QRandomGenerator>
#include <QStringBuilder>
#include <QVBoxLayout> #include <QVBoxLayout>
namespace {
using namespace chatterino::literals;
const std::initializer_list<QString> 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 { namespace chatterino {
using namespace literals;
LastRunCrashDialog::LastRunCrashDialog() LastRunCrashDialog::LastRunCrashDialog()
{ {
this->setWindowFlag(Qt::WindowContextHelpButtonHint, false); this->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
this->setWindowTitle("Chatterino"); this->setWindowTitle(u"Chatterino - " % randomMessage());
auto layout = auto layout =
LayoutCreator<LastRunCrashDialog>(this).setLayoutType<QVBoxLayout>(); LayoutCreator<LastRunCrashDialog>(this).setLayoutType<QVBoxLayout>();
layout.emplace<QLabel>("The application wasn't terminated properly the " QString text =
"last time it was executed."); u"Chatterino unexpectedly crashed and restarted. "_s
"<i>You can disable automatic restarts in the settings.</i><br><br>";
#ifdef CHATTERINO_WITH_CRASHPAD
auto reportsDir =
QDir(getPaths()->crashdumpDirectory).filePath(u"reports"_s);
text += u"A <b>crash report</b> has been saved to "
"<a href=\"file:///" %
reportsDir % u"\">" % reportsDir % u"</a>.<br>";
if (getArgs().exceptionCode)
{
text += u"The last run crashed with code <code>0x" %
QString::number(*getArgs().exceptionCode, 16) % u"</code>";
if (getArgs().exceptionMessage)
{
text += u" (" % *getArgs().exceptionMessage % u")";
}
text += u".<br>"_s;
}
text +=
"Crash reports are <b>only stored locally</b> and never uploaded.<br>"
"<br>Please <a "
"href=\"https://github.com/Chatterino/chatterino2/issues/new\">report "
"the crash</a> "
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"<br>For more information, <a href=\"https://wiki.chatterino.com/Crash%20Analysis/\">consult the wiki</a>."_s;
#endif
auto label = layout.emplace<QLabel>(text);
label->setTextInteractionFlags(Qt::TextBrowserInteraction);
label->setOpenExternalLinks(true);
label->setWordWrap(true);
layout->addSpacing(16); layout->addSpacing(16);
// auto update = layout.emplace<QLabel>();
auto buttons = layout.emplace<QDialogButtonBox>(); auto buttons = layout.emplace<QDialogButtonBox>();
// auto *installUpdateButton = buttons->addButton("Install Update", auto *okButton = buttons->addButton(u"Ok"_s, QDialogButtonBox::AcceptRole);
// 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);
QObject::connect(okButton, &QPushButton::clicked, [this] { QObject::connect(okButton, &QPushButton::clicked, [this] {
this->accept(); 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 } // namespace chatterino

View file

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <pajlada/signals/signalholder.hpp>
#include <QDialog> #include <QDialog>
namespace chatterino { namespace chatterino {
@ -9,9 +8,6 @@ class LastRunCrashDialog : public QDialog
{ {
public: public:
LastRunCrashDialog(); LastRunCrashDialog();
private:
pajlada::Signals::SignalHolder signalHolder_;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -8,6 +8,7 @@
#include "controllers/sound/ISoundController.hpp" #include "controllers/sound/ISoundController.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "singletons/NativeMessaging.hpp" #include "singletons/NativeMessaging.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
@ -875,8 +876,14 @@ void GeneralPage::initLayout(GeneralPageView &layout)
s.openLinksIncognito); s.openLinksIncognito);
} }
layout.addCheckbox( layout.addCustomCheckbox(
"Restart on crash", s.restartOnCrash, false, "Restart on crash (requires restart)",
[] {
return getApp()->crashHandler->shouldRecover();
},
[](bool on) {
return getApp()->crashHandler->saveShouldRecover(on);
},
"When possible, restart Chatterino if the program crashes"); "When possible, restart Chatterino if the program crashes");
#if defined(Q_OS_LINUX) && !defined(NO_QTKEYCHAIN) #if defined(Q_OS_LINUX) && !defined(NO_QTKEYCHAIN)

View file

@ -125,6 +125,28 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text,
return check; return check;
} }
QCheckBox *GeneralPageView::addCustomCheckbox(const QString &text,
const std::function<bool()> &load,
std::function<void(bool)> 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, ComboBox *GeneralPageView::addDropdown(const QString &text,
const QStringList &list, const QStringList &list,
QString toolTipText) QString toolTipText)

View file

@ -103,6 +103,11 @@ public:
/// @param inverse Inverses true to false and vice versa /// @param inverse Inverses true to false and vice versa
QCheckBox *addCheckbox(const QString &text, BoolSetting &setting, QCheckBox *addCheckbox(const QString &text, BoolSetting &setting,
bool inverse = false, QString toolTipText = {}); bool inverse = false, QString toolTipText = {});
QCheckBox *addCustomCheckbox(const QString &text,
const std::function<bool()> &load,
std::function<void(bool)> save,
const QString &toolTipText = {});
ComboBox *addDropdown(const QString &text, const QStringList &items, ComboBox *addDropdown(const QString &text, const QStringList &items,
QString toolTipText = {}); QString toolTipText = {});
ComboBox *addDropdown(const QString &text, const QStringList &items, ComboBox *addDropdown(const QString &text, const QStringList &items,

1
tools/crash-handler Submodule

@ -0,0 +1 @@
Subproject commit 9753fe802710b2df00f2287ec2e1ca78c251d085