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,
-bugprone-easily-swappable-parameters,
-cert-err58-cpp,
-modernize-avoid-c-arrays
"
CheckOptions:
- key: readability-identifier-naming.ClassCase

View file

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

View file

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

6
.gitmodules vendored
View file

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

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

View file

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

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

View file

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

View file

@ -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<Toasts>())
, imageUploader(&this->emplace<ImageUploader>())
, seventvAPI(&this->emplace<SeventvAPI>())
, crashHandler(&this->emplace<CrashHandler>())
, commands(&this->emplace<CommandController>())
, notifications(&this->emplace<NotificationController>())
@ -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();

View file

@ -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;

View file

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

View file

@ -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();

View file

@ -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 <QStringList>
#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 {
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<uint32_t>(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)

View file

@ -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<uint32_t> exceptionCode{};
/// Text version of the exception code. Potentially contains more context.
std::optional<QString> 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);

View file

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

View file

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

View file

@ -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"

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,
};
QStringSetting cachePath = {"/cache/path", ""};
BoolSetting restartOnCrash = {"/misc/restartOnCrash", false};
BoolSetting attachExtensionToAnyProcess = {
"/misc/attachExtensionToAnyProcess", false};
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/PostToThread.hpp"
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QDir>
#include <QLabel>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QRandomGenerator>
#include <QStringBuilder>
#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 {
using namespace literals;
LastRunCrashDialog::LastRunCrashDialog()
{
this->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
this->setWindowTitle("Chatterino");
this->setWindowTitle(u"Chatterino - " % randomMessage());
auto layout =
LayoutCreator<LastRunCrashDialog>(this).setLayoutType<QVBoxLayout>();
layout.emplace<QLabel>("The application wasn't terminated properly the "
"last time it was executed.");
QString text =
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);
// auto update = layout.emplace<QLabel>();
auto buttons = layout.emplace<QDialogButtonBox>();
// 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

View file

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

View file

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

View file

@ -125,6 +125,28 @@ QCheckBox *GeneralPageView::addCheckbox(const QString &text,
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,
const QStringList &list,
QString toolTipText)

View file

@ -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<bool()> &load,
std::function<void(bool)> save,
const QString &toolTipText = {});
ComboBox *addDropdown(const QString &text, const QStringList &items,
QString toolTipText = {});
ComboBox *addDropdown(const QString &text, const QStringList &items,

1
tools/crash-handler Submodule

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