forked from quickshell/quickshell
crash: add crash reporter
This commit is contained in:
parent
5040f3796c
commit
fe1d15e8f6
23 changed files with 1118 additions and 315 deletions
16
src/crash/CMakeLists.txt
Normal file
16
src/crash/CMakeLists.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
qt_add_library(quickshell-crash STATIC
|
||||
main.cpp
|
||||
interface.cpp
|
||||
handler.cpp
|
||||
)
|
||||
|
||||
qs_pch(quickshell-crash)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad)
|
||||
# only need client?? take only includes from pkg config todo
|
||||
target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client)
|
||||
|
||||
target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt6::Widgets)
|
||||
|
||||
target_link_libraries(quickshell-core PRIVATE quickshell-crash)
|
180
src/crash/handler.cpp
Normal file
180
src/crash/handler.cpp
Normal file
|
@ -0,0 +1,180 @@
|
|||
#include "handler.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <bits/types/sigset_t.h>
|
||||
#include <breakpad/client/linux/handler/exception_handler.h>
|
||||
#include <breakpad/client/linux/handler/minidump_descriptor.h>
|
||||
#include <breakpad/common/linux/linux_libc_support.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qfile.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../core/crashinfo.hpp"
|
||||
|
||||
extern char** environ; // NOLINT
|
||||
|
||||
using namespace google_breakpad;
|
||||
|
||||
namespace qs::crash {
|
||||
|
||||
Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
|
||||
struct CrashHandlerPrivate {
|
||||
ExceptionHandler* exceptionHandler = nullptr;
|
||||
int minidumpFd = -1;
|
||||
int infoFd = -1;
|
||||
|
||||
static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded);
|
||||
};
|
||||
|
||||
CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {}
|
||||
|
||||
void CrashHandler::init() {
|
||||
// MinidumpDescriptor has no move constructor and the copy constructor breaks fds.
|
||||
auto createHandler = [this](const MinidumpDescriptor& desc) {
|
||||
this->d->exceptionHandler = new ExceptionHandler(
|
||||
desc,
|
||||
nullptr,
|
||||
&CrashHandlerPrivate::minidumpCallback,
|
||||
this->d,
|
||||
true,
|
||||
-1
|
||||
);
|
||||
};
|
||||
|
||||
qCDebug(logCrashHandler) << "Starting crash handler...";
|
||||
|
||||
this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC);
|
||||
|
||||
if (this->d->minidumpFd == -1) {
|
||||
qCCritical(logCrashHandler
|
||||
) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory.";
|
||||
createHandler(MinidumpDescriptor("."));
|
||||
} else {
|
||||
qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd
|
||||
<< "for holding possible minidumps.";
|
||||
createHandler(MinidumpDescriptor(this->d->minidumpFd));
|
||||
}
|
||||
|
||||
qCInfo(logCrashHandler) << "Crash handler initialized.";
|
||||
}
|
||||
|
||||
void CrashHandler::setInstanceInfo(const InstanceInfo& info) {
|
||||
this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
|
||||
|
||||
if (this->d->infoFd == -1) {
|
||||
qCCritical(logCrashHandler
|
||||
) << "Failed to allocate instance info memfd, crash recovery will not work.";
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file;
|
||||
file.open(this->d->infoFd, QFile::ReadWrite);
|
||||
|
||||
QDataStream ds(&file);
|
||||
ds << info;
|
||||
file.flush();
|
||||
|
||||
qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd;
|
||||
}
|
||||
|
||||
CrashHandler::~CrashHandler() {
|
||||
delete this->d->exceptionHandler;
|
||||
delete this->d;
|
||||
}
|
||||
|
||||
bool CrashHandlerPrivate::minidumpCallback(
|
||||
const MinidumpDescriptor& /*descriptor*/,
|
||||
void* context,
|
||||
bool /*success*/
|
||||
) {
|
||||
// A fork that just dies to ensure the coredump is caught by the system.
|
||||
auto coredumpPid = fork();
|
||||
|
||||
if (coredumpPid == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* self = static_cast<CrashHandlerPrivate*>(context);
|
||||
|
||||
auto exe = std::array<char, 4096>();
|
||||
if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) {
|
||||
perror("Failed to find crash reporter executable.\n");
|
||||
_exit(-1);
|
||||
}
|
||||
|
||||
auto arg = std::array<char*, 2> {exe.data(), nullptr};
|
||||
|
||||
auto env = std::array<char*, 4096>();
|
||||
auto envi = 0;
|
||||
|
||||
auto infoFd = dup(self->infoFd);
|
||||
auto infoFdStr = std::array<char, 38>();
|
||||
memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30);
|
||||
if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10);
|
||||
env[envi++] = infoFdStr.data();
|
||||
|
||||
auto corePidStr = std::array<char, 39>();
|
||||
memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31);
|
||||
if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10);
|
||||
env[envi++] = corePidStr.data();
|
||||
|
||||
auto populateEnv = [&]() {
|
||||
auto senvi = 0;
|
||||
while (envi < 4095) {
|
||||
env[envi++] = environ[senvi++]; // NOLINT
|
||||
}
|
||||
|
||||
env[envi] = nullptr;
|
||||
};
|
||||
|
||||
sigset_t sigset;
|
||||
sigemptyset(&sigset); // NOLINT (include)
|
||||
sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT
|
||||
|
||||
auto pid = fork();
|
||||
|
||||
if (pid == -1) {
|
||||
perror("Failed to fork and launch crash reporter.\n");
|
||||
return false;
|
||||
} else if (pid == 0) {
|
||||
// dup to remove CLOEXEC
|
||||
// if already -1 will return -1
|
||||
auto dumpFd = dup(self->minidumpFd);
|
||||
auto logFd = dup(CrashInfo::INSTANCE.logFd);
|
||||
|
||||
// allow up to 10 digits, which should never happen
|
||||
auto dumpFdStr = std::array<char, 38>();
|
||||
auto logFdStr = std::array<char, 37>();
|
||||
|
||||
memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30);
|
||||
memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29);
|
||||
|
||||
if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10);
|
||||
if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10);
|
||||
|
||||
env[envi++] = dumpFdStr.data();
|
||||
env[envi++] = logFdStr.data();
|
||||
|
||||
populateEnv();
|
||||
execve(exe.data(), arg.data(), env.data());
|
||||
|
||||
perror("Failed to launch crash reporter.\n");
|
||||
_exit(-1);
|
||||
} else {
|
||||
populateEnv();
|
||||
execve(exe.data(), arg.data(), env.data());
|
||||
|
||||
perror("Failed to relaunch quickshell.\n");
|
||||
_exit(-1);
|
||||
}
|
||||
|
||||
return false; // should make sure it hits the system coredump handler
|
||||
}
|
||||
|
||||
} // namespace qs::crash
|
23
src/crash/handler.hpp
Normal file
23
src/crash/handler.hpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <qtclasshelpermacros.h>
|
||||
|
||||
#include "../core/crashinfo.hpp"
|
||||
namespace qs::crash {
|
||||
|
||||
struct CrashHandlerPrivate;
|
||||
|
||||
class CrashHandler {
|
||||
public:
|
||||
explicit CrashHandler();
|
||||
~CrashHandler();
|
||||
Q_DISABLE_COPY_MOVE(CrashHandler);
|
||||
|
||||
void init();
|
||||
void setInstanceInfo(const InstanceInfo& info);
|
||||
|
||||
private:
|
||||
CrashHandlerPrivate* d;
|
||||
};
|
||||
|
||||
} // namespace qs::crash
|
97
src/crash/interface.cpp
Normal file
97
src/crash/interface.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include "interface.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qapplication.h>
|
||||
#include <qboxlayout.h>
|
||||
#include <qdesktopservices.h>
|
||||
#include <qfont.h>
|
||||
#include <qfontinfo.h>
|
||||
#include <qlabel.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpushbutton.h>
|
||||
#include <qwidget.h>
|
||||
|
||||
#include "build.hpp"
|
||||
|
||||
class ReportLabel: public QWidget {
|
||||
public:
|
||||
ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) {
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(new QLabel(label, this));
|
||||
|
||||
auto* cl = new QLabel(content, this);
|
||||
cl->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
layout->addWidget(cl);
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
};
|
||||
|
||||
CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
|
||||
: reportFolder(std::move(reportFolder)) {
|
||||
this->setWindowFlags(Qt::Window);
|
||||
|
||||
auto textHeight = QFontInfo(QFont()).pixelSize();
|
||||
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
|
||||
mainLayout->addWidget(new QLabel(
|
||||
"<u>Quickshell has crashed. Please submit a bug report to help us fix it.</u>",
|
||||
this
|
||||
));
|
||||
|
||||
mainLayout->addSpacing(textHeight);
|
||||
|
||||
mainLayout->addWidget(new QLabel("General information", this));
|
||||
mainLayout->addWidget(new ReportLabel("Git Revision:", GIT_REVISION, this));
|
||||
mainLayout->addWidget(new ReportLabel("Crashed process ID:", QString::number(pid), this));
|
||||
mainLayout->addWidget(new ReportLabel("Crash report folder:", this->reportFolder, this));
|
||||
mainLayout->addSpacing(textHeight);
|
||||
|
||||
mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email."));
|
||||
|
||||
mainLayout->addWidget(new ReportLabel(
|
||||
"Github:",
|
||||
"https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml",
|
||||
this
|
||||
));
|
||||
|
||||
mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this));
|
||||
|
||||
auto* buttons = new QWidget(this);
|
||||
buttons->setMinimumWidth(900);
|
||||
auto* buttonLayout = new QHBoxLayout(buttons);
|
||||
buttonLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto* reportButton = new QPushButton("Open report page", buttons);
|
||||
reportButton->setDefault(true);
|
||||
QObject::connect(reportButton, &QPushButton::clicked, this, &CrashReporterGui::openReportUrl);
|
||||
buttonLayout->addWidget(reportButton);
|
||||
|
||||
auto* openFolderButton = new QPushButton("Open crash folder", buttons);
|
||||
QObject::connect(openFolderButton, &QPushButton::clicked, this, &CrashReporterGui::openFolder);
|
||||
buttonLayout->addWidget(openFolderButton);
|
||||
|
||||
auto* cancelButton = new QPushButton("Exit", buttons);
|
||||
QObject::connect(cancelButton, &QPushButton::clicked, this, &CrashReporterGui::cancel);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
mainLayout->addStretch();
|
||||
this->setFixedSize(this->sizeHint());
|
||||
}
|
||||
|
||||
void CrashReporterGui::openFolder() {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder));
|
||||
}
|
||||
|
||||
void CrashReporterGui::openReportUrl() {
|
||||
QDesktopServices::openUrl(
|
||||
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml")
|
||||
);
|
||||
}
|
||||
|
||||
void CrashReporterGui::cancel() { QApplication::quit(); }
|
17
src/crash/interface.hpp
Normal file
17
src/crash/interface.hpp
Normal file
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <qwidget.h>
|
||||
|
||||
class CrashReporterGui: public QWidget {
|
||||
public:
|
||||
CrashReporterGui(QString reportFolder, int pid);
|
||||
|
||||
private slots:
|
||||
void openFolder();
|
||||
|
||||
static void openReportUrl();
|
||||
static void cancel();
|
||||
|
||||
private:
|
||||
QString reportFolder;
|
||||
};
|
165
src/crash/main.cpp
Normal file
165
src/crash/main.cpp
Normal file
|
@ -0,0 +1,165 @@
|
|||
#include "main.hpp"
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <qapplication.h>
|
||||
#include <qconfig.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtextstream.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "../core/crashinfo.hpp"
|
||||
#include "../core/logging.hpp"
|
||||
#include "../core/paths.hpp"
|
||||
#include "build.hpp"
|
||||
#include "interface.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
|
||||
|
||||
void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instanceInfo);
|
||||
|
||||
void qsCheckCrash(int argc, char** argv) {
|
||||
auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD");
|
||||
if (fd.isEmpty()) return;
|
||||
auto app = QApplication(argc, argv);
|
||||
|
||||
InstanceInfo instance;
|
||||
|
||||
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
|
||||
|
||||
{
|
||||
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
|
||||
|
||||
QFile file;
|
||||
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
|
||||
file.seek(0);
|
||||
|
||||
auto ds = QDataStream(&file);
|
||||
ds >> instance;
|
||||
}
|
||||
|
||||
LogManager::init(!instance.noColor, false);
|
||||
auto crashDir = QsPaths::crashDir(instance.shellId, instance.launchTime);
|
||||
|
||||
qCInfo(logCrashReporter) << "Starting crash reporter...";
|
||||
|
||||
recordCrashInfo(crashDir, instance);
|
||||
|
||||
auto gui = CrashReporterGui(crashDir.path(), crashProc);
|
||||
gui.show();
|
||||
exit(QApplication::exec()); // NOLINT
|
||||
}
|
||||
|
||||
int tryDup(int fd, const QString& path) {
|
||||
QFile sourceFile;
|
||||
if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
|
||||
qCCritical(logCrashReporter) << "Failed to open source fd for duplication.";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto destFile = QFile(path);
|
||||
if (!destFile.open(QFile::WriteOnly)) {
|
||||
qCCritical(logCrashReporter) << "Failed to open dest file for duplication.";
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto size = sourceFile.size();
|
||||
off_t offset = 0;
|
||||
ssize_t count = 0;
|
||||
|
||||
sourceFile.seek(0);
|
||||
|
||||
while (count != size) {
|
||||
auto r = sendfile(destFile.handle(), sourceFile.handle(), &offset, sourceFile.size());
|
||||
if (r == -1) {
|
||||
qCCritical(logCrashReporter).nospace()
|
||||
<< "Failed to duplicate fd " << fd << " with error code " << errno
|
||||
<< ". Error: " << qt_error_string();
|
||||
return 3;
|
||||
} else {
|
||||
count += r;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
|
||||
qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path();
|
||||
|
||||
if (!crashDir.mkpath(".")) {
|
||||
qCCritical(logCrashReporter) << "Failed to create folder" << crashDir
|
||||
<< "to save crash information.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
|
||||
auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt();
|
||||
auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt();
|
||||
|
||||
qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd;
|
||||
auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp"));
|
||||
if (dumpDupStatus != 0) {
|
||||
qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus;
|
||||
}
|
||||
|
||||
qCDebug(logCrashReporter) << "Saving log from fd" << logFd;
|
||||
auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog"));
|
||||
if (logDupStatus != 0) {
|
||||
qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus;
|
||||
}
|
||||
|
||||
{
|
||||
auto extraInfoFile = QFile(crashDir.filePath("info.txt"));
|
||||
if (!extraInfoFile.open(QFile::WriteOnly)) {
|
||||
qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
|
||||
} else {
|
||||
auto stream = QTextStream(&extraInfoFile);
|
||||
stream << "===== Quickshell Crash =====\n";
|
||||
stream << "Git Revision: " << GIT_REVISION << '\n';
|
||||
stream << "Crashed process ID: " << crashProc << '\n';
|
||||
stream << "Run ID: " << QString("run-%1").arg(instance.launchTime.toMSecsSinceEpoch())
|
||||
<< '\n';
|
||||
|
||||
stream << "\n===== Shell Information =====\n";
|
||||
stream << "Shell ID: " << instance.shellId << '\n';
|
||||
stream << "Config Path: " << instance.configPath << '\n';
|
||||
|
||||
stream << "\n===== Report Integrity =====\n";
|
||||
stream << "Minidump save status: " << dumpDupStatus << '\n';
|
||||
stream << "Log save status: " << logDupStatus << '\n';
|
||||
|
||||
stream << "\n===== System Information =====\n";
|
||||
stream << "Qt Version: " << QT_VERSION_STR << "\n\n";
|
||||
|
||||
stream << "/etc/os-release:";
|
||||
auto osReleaseFile = QFile("/etc/os-release");
|
||||
if (osReleaseFile.open(QFile::ReadOnly)) {
|
||||
stream << '\n' << osReleaseFile.readAll() << '\n';
|
||||
osReleaseFile.close();
|
||||
} else {
|
||||
stream << "FAILED TO OPEN\n";
|
||||
}
|
||||
|
||||
stream << "/etc/lsb-release:";
|
||||
auto lsbReleaseFile = QFile("/etc/lsb-release");
|
||||
if (lsbReleaseFile.open(QFile::ReadOnly)) {
|
||||
stream << '\n' << lsbReleaseFile.readAll() << '\n';
|
||||
lsbReleaseFile.close();
|
||||
} else {
|
||||
stream << "FAILED TO OPEN\n";
|
||||
}
|
||||
|
||||
extraInfoFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(logCrashReporter) << "Recorded crash information.";
|
||||
}
|
3
src/crash/main.hpp
Normal file
3
src/crash/main.hpp
Normal file
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
void qsCheckCrash(int argc, char** argv);
|
Loading…
Add table
Add a link
Reference in a new issue