crash: add crash reporter

This commit is contained in:
outfoxxed 2024-08-20 00:41:20 -07:00
parent 5040f3796c
commit fe1d15e8f6
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
23 changed files with 1118 additions and 315 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: true

72
.github/ISSUE_TEMPLATE/crash.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Crash Report
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
- type: textarea
id: crashinfo
attributes:
label: General crash information
description: |
Paste the contents of the `info.txt` file in your crash folder here.
value: "<details> <summary>General information</summary>
```
<Paste the contents of the file here inside of the triple backticks>
```
</details>"
validations:
required: true
- type: textarea
id: userinfo
attributes:
label: What caused the crash
description: |
Any information likely to help debug the crash. What were you doing when the crash occurred,
what changes did you make, can you get it to happen again?
- type: textarea
id: dump
attributes:
label: Minidump
description: |
Attach `minidump.dmp` here. If it is too big to upload, compress it.
You may skip this step if quickshell crashed while processing a password
or other sensitive information. If you skipped it write why instead.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
Attach your configuration here, preferrably in full (not just one file).
Compress it into a zip, tar, etc.
This will help us reproduce the crash ourselves.
- type: textarea
id: bt
attributes:
label: Backtrace
description: |
If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
we would appreciate one. (You may have gdb installed without knowing it)
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.

View file

@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF)
option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
option(CRASH_REPORTER "Enable the crash reporter" ON)
option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON)
option(SOCKETS "Enable unix socket support" ON) option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON) option(WAYLAND "Enable wayland support" ON)
@ -29,6 +30,7 @@ option(SERVICE_UPOWER "UPower service" ON)
option(SERVICE_NOTIFICATIONS "Notification server" ON) option(SERVICE_NOTIFICATIONS "Notification server" ON)
message(STATUS "Quickshell configuration") message(STATUS "Quickshell configuration")
message(STATUS " Crash reporter: ${CRASH_REPORTER}")
message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " Jemalloc: ${USE_JEMALLOC}")
message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Build tests: ${BUILD_TESTING}")
message(STATUS " Sockets: ${SOCKETS}") message(STATUS " Sockets: ${SOCKETS}")

View file

@ -9,6 +9,7 @@
ninja, ninja,
qt6, qt6,
cli11, cli11,
breakpad,
jemalloc, jemalloc,
wayland, wayland,
wayland-protocols, wayland-protocols,
@ -28,6 +29,7 @@
else "unknown"), else "unknown"),
debug ? false, debug ? false,
withCrashReporter ? true,
withJemalloc ? true, # masks heap fragmentation withJemalloc ? true, # masks heap fragmentation
withQtSvg ? true, withQtSvg ? true,
withWayland ? true, withWayland ? true,
@ -55,6 +57,7 @@
qt6.qtdeclarative qt6.qtdeclarative
cli11 cli11
] ]
++ (lib.optional withCrashReporter breakpad)
++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withJemalloc jemalloc)
++ (lib.optional withQtSvg qt6.qtsvg) ++ (lib.optional withQtSvg qt6.qtsvg)
++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) ++ (lib.optionals withWayland [ qt6.qtwayland wayland ])
@ -67,6 +70,7 @@
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ] cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ]
++ lib.optional (!withCrashReporter) "-DCRASH_REPORTER=OFF"
++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF"
++ lib.optional (!withWayland) "-DWAYLAND=OFF" ++ lib.optional (!withWayland) "-DWAYLAND=OFF"
++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF"

View file

@ -5,6 +5,10 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(core) add_subdirectory(core)
add_subdirectory(io) add_subdirectory(io)
if (CRASH_REPORTER)
add_subdirectory(crash)
endif()
if (DBUS) if (DBUS)
add_subdirectory(dbus) add_subdirectory(dbus)
endif() endif()

View file

@ -41,9 +41,19 @@ qt_add_library(quickshell-core STATIC
clock.cpp clock.cpp
logging.cpp logging.cpp
paths.cpp paths.cpp
crashinfo.cpp
common.cpp
) )
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") if (CRASH_REPORTER)
set(CRASH_REPORTER_DEF 1)
endif()
add_library(quickshell-build INTERFACE)
configure_file(build.hpp.in build.hpp)
target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(quickshell-core PRIVATE quickshell-build)
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11)

6
src/core/build.hpp.in Normal file
View file

@ -0,0 +1,6 @@
#pragma once
// NOLINTBEGIN
#define GIT_REVISION "@GIT_REVISION@"
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
// NOLINTEND

9
src/core/common.cpp Normal file
View file

@ -0,0 +1,9 @@
#include "common.hpp"
#include <qdatetime.h>
namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
}

11
src/core/common.hpp Normal file
View file

@ -0,0 +1,11 @@
#pragma once
#include <qdatetime.h>
namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
};
} // namespace qs

19
src/core/crashinfo.cpp Normal file
View file

@ -0,0 +1,19 @@
#include "crashinfo.hpp"
#include <qdatastream.h>
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
stream << info.configPath << info.shellId << info.launchTime << info.noColor;
return stream;
}
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
stream >> info.configPath >> info.shellId >> info.launchTime >> info.noColor;
return stream;
}
namespace qs::crash {
CrashInfo CrashInfo::INSTANCE = {}; // NOLINT
}

26
src/core/crashinfo.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <qdatetime.h>
#include <qstring.h>
struct InstanceInfo {
QString configPath;
QString shellId;
QString initialWorkdir;
QDateTime launchTime;
bool noColor = false;
bool sparseLogsOnly = false;
};
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info);
namespace qs::crash {
struct CrashInfo {
int logFd = -1;
static CrashInfo INSTANCE; // NOLINT
};
} // namespace qs::crash

View file

@ -24,6 +24,7 @@
#include <sys/mman.h> #include <sys/mman.h>
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include "crashinfo.hpp"
#include "logging_p.hpp" #include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT #include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp" #include "paths.hpp"
@ -198,14 +199,16 @@ void ThreadLogging::init() {
if (logMfd != -1) { if (logMfd != -1) {
this->file = new QFile(); this->file = new QFile();
this->file->open(logMfd, QFile::WriteOnly, QFile::AutoCloseHandle); this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file); this->fileStream.setDevice(this->file);
} }
if (dlogMfd != -1) { if (dlogMfd != -1) {
crash::CrashInfo::INSTANCE.logFd = dlogMfd;
this->detailedFile = new QFile(); this->detailedFile = new QFile();
// buffered by WriteBuffer // buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::WriteOnly | QFile::Unbuffered, QFile::AutoCloseHandle); this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
this->detailedWriter.setDevice(this->detailedFile); this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) { if (!this->detailedWriter.writeHeader()) {
@ -245,7 +248,7 @@ void ThreadLogging::initFs() {
auto* file = new QFile(path); auto* file = new QFile(path);
auto* detailedFile = new QFile(detailedPath); auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::WriteOnly | QFile::Truncate)) { if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging qCCritical(logLogging
) << "Could not start filesystem logger as the log file could not be created:" ) << "Could not start filesystem logger as the log file could not be created:"
<< path; << path;
@ -256,7 +259,7 @@ void ThreadLogging::initFs() {
} }
// buffered by WriteBuffer // buffered by WriteBuffer
if (!detailedFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered)) { if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging qCCritical(logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:" ) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath; << detailedPath;
@ -287,6 +290,8 @@ void ThreadLogging::initFs() {
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
} }
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
this->detailedFile = detailedFile; this->detailedFile = detailedFile;
this->detailedWriter.setDevice(detailedFile); this->detailedWriter.setDevice(detailedFile);

View file

@ -1,13 +1,17 @@
#include "main.hpp" #include "main.hpp"
#include <cstdlib>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <CLI/App.hpp> #include <CLI/App.hpp>
#include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes #include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes
#include <CLI/Error.hpp>
#include <CLI/Validators.hpp> #include <CLI/Validators.hpp>
#include <qapplication.h> #include <qapplication.h>
#include <qcoreapplication.h> #include <qcoreapplication.h>
#include <qcryptographichash.h> #include <qcryptographichash.h>
#include <qdatastream.h>
#include <qdatetime.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qguiapplication.h> #include <qguiapplication.h>
@ -24,162 +28,164 @@
#include <qtextstream.h> #include <qtextstream.h>
#include <qtpreprocessorsupport.h> #include <qtpreprocessorsupport.h>
#include "build.hpp"
#include "common.hpp"
#include "crashinfo.hpp"
#include "logging.hpp" #include "logging.hpp"
#include "paths.hpp" #include "paths.hpp"
#include "plugin.hpp" #include "plugin.hpp"
#include "rootwrapper.hpp" #include "rootwrapper.hpp"
#if CRASH_REPORTER
#include "../crash/handler.hpp"
#include "../crash/main.hpp"
#endif
int qs_main(int argc, char** argv) { struct CommandInfo {
QString configPath;
QString manifestPath;
QString configName;
QString& initialWorkdir;
int& debugPort;
bool& waitForDebug;
bool& printInfo;
bool& noColor;
bool& sparseLogsOnly;
};
auto qArgC = 1; void processCommand(int argc, char** argv, CommandInfo& info) {
auto* qArgV = argv; auto app = CLI::App("");
auto noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); class QStringOption {
public:
QStringOption() = default;
QStringOption& operator=(const std::string& str) {
this->str = QString::fromStdString(str);
return *this;
}
QString workingDirectory; QString& operator*() { return this->str; }
QString configFilePath;
QString shellId;
auto printInfo = false;
auto debugPort = -1; private:
auto waitForDebug = false; QString str;
};
auto useQApplication = false; class QStringRefOption {
auto nativeTextRendering = false; public:
auto desktopSettingsAware = true; QStringRefOption(QString* str): str(str) {}
QHash<QString, QString> envOverrides; QStringRefOption& operator=(const std::string& str) {
*this->str = QString::fromStdString(str);
return *this;
}
{ private:
auto app = CLI::App(""); QString* str;
};
class QStringOption { /// ---
public: QStringRefOption path(&info.configPath);
QStringOption() = default; QStringRefOption manifest(&info.manifestPath);
QStringOption& operator=(const std::string& str) { QStringRefOption config(&info.configName);
this->str = QString::fromStdString(str); QStringRefOption workdirRef(&info.initialWorkdir);
return *this;
}
QString& operator*() { return this->str; } auto* selection = app.add_option_group(
"Config Selection",
"Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)"
);
private: auto* pathArg =
QString str; selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)");
};
class QStringRefOption { auto* mfArg = selection->add_option(
public: "-m,--manifest",
QStringRefOption(QString* str): str(str) {} manifest,
QStringRefOption& operator=(const std::string& str) { "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n"
*this->str = QString::fromStdString(str); "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)"
return *this; );
}
private: auto* cfgArg = selection->add_option(
QString* str; "-c,--config",
}; config,
"Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)"
);
/// --- selection->add_option("-d,--workdir", workdirRef, "Initial working directory.");
QStringOption path;
QStringOption manifest;
QStringOption config;
QStringRefOption workdirRef(&workingDirectory);
auto* selection = app.add_option_group( pathArg->excludes(mfArg, cfgArg);
"Config Selection",
"Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)"
);
auto* pathArg = /// ---
selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); auto* debug = app.add_option_group("Debugging");
auto* mfArg = selection->add_option( auto* debugPortArg = debug
"-m,--manifest", ->add_option(
manifest, "--debugport",
"Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" info.debugPort,
"(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" "Open the given port for a QML debugger to connect to."
); )
->check(CLI::Range(0, 65535));
auto* cfgArg = selection->add_option( debug
"-c,--config", ->add_flag(
config, "--waitfordebug",
"Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" info.waitForDebug,
); "Wait for a debugger to attach to the given port before launching."
)
->needs(debugPortArg);
selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); /// ---
app.add_flag("--info", info.printInfo, "Print information about the shell")
->excludes(debugPortArg);
app.add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)");
auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit.");
pathArg->excludes(mfArg, cfgArg); app.add_flag(
"--no-detailed-logs",
info.sparseLogsOnly,
"Do not enable this unless you know what you are doing."
);
/// --- /// ---
auto* debug = app.add_option_group("Debugging"); QStringOption logPath;
QStringOption logFilter;
auto logNoTime = false;
auto* debugPortArg = debug auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file.");
->add_option( readLog->add_option("path", logPath, "Path to the log file to read")->required();
"--debugport",
debugPort,
"Open the given port for a QML debugger to connect to."
)
->check(CLI::Range(0, 65535));
debug readLog->add_option(
->add_flag( "-f,--filter",
"--waitfordebug", logFilter,
waitForDebug, "Logging categories to display. (same syntax as QT_LOGGING_RULES)"
"Wait for a debugger to attach to the given port before launching." );
)
->needs(debugPortArg);
/// --- readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages.");
auto sparseLogsOnly = false; readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)");
app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg);
app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)");
auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit.");
app.add_flag( try {
"--no-detailed-logs", app.parse(argc, argv);
sparseLogsOnly, } catch (const CLI::ParseError& e) {
"Do not enable this unless you know what you are doing." exit(app.exit(e)); // NOLINT
); };
/// --- if (*printVersion) {
QStringOption logPath; std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl;
QStringOption logFilter; exit(0); // NOLINT
auto logNoTime = false; } else if (*readLog) {
auto file = QFile(*logPath);
auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); if (!file.open(QFile::ReadOnly)) {
readLog->add_option("path", logPath, "Path to the log file to read")->required(); qCritical() << "Failed to open log for reading:" << *logPath;
exit(-1); // NOLINT
readLog->add_option(
"-f,--filter",
logFilter,
"Logging categories to display. (same syntax as QT_LOGGING_RULES)"
);
readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages.");
readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)");
CLI11_PARSE(app, argc, argv);
const auto qApplication = QCoreApplication(qArgC, qArgV);
// Start log manager - has to happen with an active event loop or offthread can't be started.
LogManager::init(!noColor, sparseLogsOnly);
if (*printVersion) {
std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl;
return 0;
} if (*readLog) {
auto file = QFile(*logPath);
if (!file.open(QFile::ReadOnly)) {
qCritical() << "Failed to open log for reading:" << *logPath;
return -1;
} else {
qInfo() << "Reading log" << *logPath;
}
return qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1;
} else { } else {
qInfo() << "Reading log" << *logPath;
}
// NOLINTBEGIN exit( // NOLINT
qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1
);
}
}
QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo) {
// NOLINTBEGIN
#define CHECK(rname, name, level, label, expr) \ #define CHECK(rname, name, level, label, expr) \
QString name = expr; \ QString name = expr; \
if (rname.isEmpty() && !name.isEmpty()) { \ if (rname.isEmpty() && !name.isEmpty()) { \
@ -189,231 +195,341 @@ int qs_main(int argc, char** argv) {
} }
#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) #define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString())
// NOLINTEND // NOLINTEND
QString basePath; QString basePath;
int basePathLevel = 0; int basePathLevel = 0;
Q_UNUSED(basePathLevel); Q_UNUSED(basePathLevel);
{ {
// NOLINTBEGIN // NOLINTBEGIN
// clang-format off // clang-format off
CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH")); CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell")); CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
// clang-format on // clang-format on
// NOLINTEND // NOLINTEND
if (printInfo) { if (printInfo) {
// clang-format off // clang-format off
std::cout << "Base path: " << OPTSTR(basePath) << "\n"; std::cout << "Base path: " << OPTSTR(basePath) << "\n";
std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n"; std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
// clang-format on // clang-format on
} }
} }
foundbase:; foundbase:;
QString configPath; QString configPath;
int configPathLevel = 10; int configPathLevel = 10;
{ {
// NOLINTBEGIN // NOLINTBEGIN
CHECK(configPath, optionConfigPath, 0, foundpath, *path); CHECK(configPath, optionConfigPath, 0, foundpath, path);
CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
// NOLINTEND // NOLINTEND
if (printInfo) { if (printInfo) {
// clang-format off // clang-format off
std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n"; std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
// clang-format on // clang-format on
} }
} }
foundpath:; foundpath:;
QString manifestPath; QString manifestPath;
int manifestPathLevel = 10; int manifestPathLevel = 10;
{ {
// NOLINTBEGIN // NOLINTBEGIN
// clang-format off // clang-format off
CHECK(manifestPath, optionManifestPath, 0, foundmf, *manifest); CHECK(manifestPath, optionManifestPath, 0, foundmf, manifest);
CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
// clang-format on // clang-format on
// NOLINTEND // NOLINTEND
if (printInfo) { if (printInfo) {
// clang-format off // clang-format off
std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n"; std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n"; std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
// clang-format on // clang-format on
} }
} }
foundmf:; foundmf:;
QString configName; QString configName;
int configNameLevel = 10; int configNameLevel = 10;
{ {
// NOLINTBEGIN // NOLINTBEGIN
CHECK(configName, optionConfigName, 0, foundname, *config); CHECK(configName, optionConfigName, 0, foundname, config);
CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
// NOLINTEND // NOLINTEND
if (printInfo) { if (printInfo) {
// clang-format off // clang-format off
std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n"; std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
// clang-format on // clang-format on
} }
} }
foundname:; foundname:;
if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { QString configFilePath;
configFilePath = configPath;
} else if (!configName.isEmpty()) {
if (!manifestPath.isEmpty()) {
auto file = QFile(manifestPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
auto split = line.split('='); if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
if (split.length() != 2) { configFilePath = configPath;
qCritical() << "manifest line not in expected format 'name = relativepath':" } else if (!configName.isEmpty()) {
<< line; if (!manifestPath.isEmpty()) {
return -1; auto file = QFile(manifestPath);
} if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
if (split[0].trimmed() == configName) { auto split = line.split('=');
configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); if (split.length() != 2) {
goto haspath; // NOLINT qCritical() << "manifest line not in expected format 'name = relativepath':" << line;
} exit(-1); // NOLINT
} }
qCritical() << "configuration" << configName << "not found in manifest" << manifestPath; if (split[0].trimmed() == configName) {
return -1; configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
} else if (manifestPathLevel < 2) { goto foundp;
qCritical() << "cannot open config manifest at" << manifestPath;
return -1;
} }
} }
{ qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
auto basePathInfo = QFileInfo(basePath); exit(-1); // NOLINT
if (!basePathInfo.exists()) { } else if (manifestPathLevel < 2) {
qCritical() << "base path does not exist:" << basePath; qCritical() << "cannot open config manifest at" << manifestPath;
return -1; exit(-1); // NOLINT
} else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { }
qCritical() << "base path is not a directory" << basePath; }
return -1;
}
auto dir = QDir(basePath); {
for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { auto basePathInfo = QFileInfo(basePath);
if (entry == configName) { if (!basePathInfo.exists()) {
configFilePath = dir.filePath(entry); qCritical() << "base path does not exist:" << basePath;
goto haspath; // NOLINT exit(-1); // NOLINT
} } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
} qCritical() << "base path is not a directory" << basePath;
exit(-1); // NOLINT
}
qCritical() << "no directory named " << configName << "found in base path" << basePath; auto dir = QDir(basePath);
return -1; for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
if (entry == configName) {
configFilePath = dir.filePath(entry);
goto foundp;
} }
haspath:;
} else {
configFilePath = basePath;
} }
auto configFile = QFileInfo(configFilePath); qCritical() << "no directory named " << configName << "found in base path" << basePath;
if (!configFile.exists()) { exit(-1); // NOLINT
qCritical() << "config path does not exist:" << configFilePath; }
return -1; } else {
} configFilePath = basePath;
}
if (configFile.isDir()) { foundp:;
configFilePath = QDir(configFilePath).filePath("shell.qml"); auto configFile = QFileInfo(configFilePath);
} if (!configFile.exists()) {
qCritical() << "config path does not exist:" << configFilePath;
exit(-1); // NOLINT
}
configFile = QFileInfo(configFilePath); if (configFile.isDir()) {
if (!configFile.exists()) { configFilePath = QDir(configFilePath).filePath("shell.qml");
qCritical() << "no shell.qml found in config path:" << configFilePath; }
return -1;
} else if (configFile.isDir()) {
qCritical() << "shell.qml is a directory:" << configFilePath;
return -1;
}
configFilePath = QFileInfo(configFilePath).canonicalFilePath(); configFile = QFileInfo(configFilePath);
configFile = QFileInfo(configFilePath); if (!configFile.exists()) {
if (!configFile.exists()) { qCritical() << "no shell.qml found in config path:" << configFilePath;
qCritical() << "config file does not exist:" << configFilePath; exit(-1); // NOLINT
return -1; } else if (configFile.isDir()) {
} else if (configFile.isDir()) { qCritical() << "shell.qml is a directory:" << configFilePath;
qCritical() << "config file is a directory:" << configFilePath; exit(-1); // NOLINT
return -1; }
}
configFilePath = QFileInfo(configFilePath).canonicalFilePath();
configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "config file does not exist:" << configFilePath;
exit(-1); // NOLINT
} else if (configFile.isDir()) {
qCritical() << "config file is a directory:" << configFilePath;
exit(-1); // NOLINT
}
#undef CHECK #undef CHECK
#undef OPTSTR #undef OPTSTR
shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); return configFilePath;
}
qInfo() << "Config file path:" << configFilePath; int qs_main(int argc, char** argv) {
#if CRASH_REPORTER
qsCheckCrash(argc, argv);
auto crashHandler = qs::crash::CrashHandler();
#endif
if (!QFile(configFilePath).exists()) { auto qArgC = 1;
qCritical() << "config file does not exist"; auto* qArgV = argv;
return -1;
QString configFilePath;
QString initialWorkdir;
QString shellId;
int debugPort = -1;
bool waitForDebug = false;
bool printInfo = false;
bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR");
bool sparseLogsOnly = false;
auto useQApplication = false;
auto nativeTextRendering = false;
auto desktopSettingsAware = true;
QHash<QString, QString> envOverrides;
{
const auto qApplication = QCoreApplication(qArgC, qArgV);
#if CRASH_REPORTER
auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD");
if (!lastInfoFdStr.isEmpty()) {
auto lastInfoFd = lastInfoFdStr.toInt();
QFile file;
file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
file.seek(0);
auto ds = QDataStream(&file);
InstanceInfo info;
ds >> info;
configFilePath = info.configPath;
initialWorkdir = info.initialWorkdir;
noColor = info.noColor;
sparseLogsOnly = info.sparseLogsOnly;
LogManager::init(!noColor, sparseLogsOnly);
qCritical().nospace() << "Quickshell has crashed under pid "
<< qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt()
<< " (Coredumps will be available under that pid.)";
qCritical() << "Further crash information is stored under"
<< QsPaths::crashDir(info.shellId, info.launchTime).path();
if (info.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) {
qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid "
"a crash loop.";
return 0;
} else {
qCritical() << "Quickshell has been restarted.";
} }
auto file = QFile(configFilePath); crashHandler.init();
if (!file.open(QFile::ReadOnly | QFile::Text)) { } else
qCritical() << "could not open config file"; #endif
return -1; {
}
auto stream = QTextStream(&file); auto command = CommandInfo {
while (!stream.atEnd()) { .initialWorkdir = initialWorkdir,
auto line = stream.readLine().trimmed(); .debugPort = debugPort,
if (line.startsWith("//@ pragma ")) { .waitForDebug = waitForDebug,
auto pragma = line.sliced(11).trimmed(); .printInfo = printInfo,
.noColor = noColor,
.sparseLogsOnly = sparseLogsOnly,
};
if (pragma == "UseQApplication") useQApplication = true; processCommand(argc, argv, command);
else if (pragma == "NativeTextRendering") nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false;
else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4);
auto splitIdx = envPragma.indexOf('=');
if (splitIdx == -1) { // Start log manager - has to happen with an active event loop or offthread can't be started.
qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; LogManager::init(!noColor, sparseLogsOnly);
return -1;
}
auto var = envPragma.sliced(0, splitIdx).trimmed(); #if CRASH_REPORTER
auto val = envPragma.sliced(splitIdx + 1).trimmed(); // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but
envOverrides.insert(var, val); // this can be moved if it happens.
} else if (pragma.startsWith("ShellId ")) { crashHandler.init();
shellId = pragma.sliced(8).trimmed(); #endif
} else {
qCritical() << "Unrecognized pragma" << pragma; configFilePath = commandConfigPath(
command.configPath,
command.manifestPath,
command.configName,
command.printInfo
);
}
shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
qInfo() << "Config file path:" << configFilePath;
if (!QFile(configFilePath).exists()) {
qCritical() << "config file does not exist";
return -1;
}
auto file = QFile(configFilePath);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCritical() << "could not open config file";
return -1;
}
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine().trimmed();
if (line.startsWith("//@ pragma ")) {
auto pragma = line.sliced(11).trimmed();
if (pragma == "UseQApplication") useQApplication = true;
else if (pragma == "NativeTextRendering") nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false;
else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4);
auto splitIdx = envPragma.indexOf('=');
if (splitIdx == -1) {
qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
return -1; return -1;
} }
} else if (line.startsWith("import")) break;
}
file.close(); auto var = envPragma.sliced(0, splitIdx).trimmed();
auto val = envPragma.sliced(splitIdx + 1).trimmed();
envOverrides.insert(var, val);
} else if (pragma.startsWith("ShellId ")) {
shellId = pragma.sliced(8).trimmed();
} else {
qCritical() << "Unrecognized pragma" << pragma;
return -1;
}
} else if (line.startsWith("import")) break;
} }
file.close();
} }
qInfo() << "Shell ID:" << shellId; qInfo() << "Shell ID:" << shellId;
if (printInfo) return 0; if (printInfo) return 0;
#if CRASH_REPORTER
crashHandler.setInstanceInfo(InstanceInfo {
.configPath = configFilePath,
.shellId = shellId,
.initialWorkdir = initialWorkdir,
.launchTime = qs::Common::LAUNCH_TIME,
.noColor = noColor,
.sparseLogsOnly = sparseLogsOnly,
});
#endif
for (auto [var, val]: envOverrides.asKeyValueRange()) { for (auto [var, val]: envOverrides.asKeyValueRange()) {
qputenv(var.toUtf8(), val.toUtf8()); qputenv(var.toUtf8(), val.toUtf8());
} }
@ -484,8 +600,8 @@ int qs_main(int argc, char** argv) {
QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait);
} }
if (!workingDirectory.isEmpty()) { if (!initialWorkdir.isEmpty()) {
QDir::setCurrent(workingDirectory); QDir::setCurrent(initialWorkdir);
} }
QuickshellPlugin::initPlugins(); QuickshellPlugin::initPlugins();

View file

@ -9,6 +9,8 @@
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <unistd.h> #include <unistd.h>
#include "common.hpp"
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
QsPaths* QsPaths::instance() { QsPaths* QsPaths::instance() {
@ -18,6 +20,15 @@ QsPaths* QsPaths::instance() {
void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); } void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); }
QDir QsPaths::crashDir(const QString& shellId, const QDateTime& launchTime) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("crashes"));
dir = QDir(dir.filePath(shellId));
dir = QDir(dir.filePath(QString("run-%1").arg(launchTime.toMSecsSinceEpoch())));
return dir;
}
QDir* QsPaths::cacheDir() { QDir* QsPaths::cacheDir() {
if (this->cacheState == DirState::Unknown) { if (this->cacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
@ -73,7 +84,7 @@ QDir* QsPaths::instanceRunDir() {
this->instanceRunState = DirState::Failed; this->instanceRunState = DirState::Failed;
} else { } else {
this->mInstanceRunDir = this->mInstanceRunDir =
runtimeDir->filePath(QString("run-%1").arg(QDateTime::currentMSecsSinceEpoch())); runtimeDir->filePath(QString("run-%1").arg(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch()));
qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path();

View file

@ -1,10 +1,12 @@
#pragma once #pragma once
#include <qdatetime.h>
#include <qdir.h> #include <qdir.h>
class QsPaths { class QsPaths {
public: public:
static QsPaths* instance(); static QsPaths* instance();
static void init(QString shellId); static void init(QString shellId);
static QDir crashDir(const QString& shellId, const QDateTime& launchTime);
QDir* cacheDir(); QDir* cacheDir();
QDir* runDir(); QDir* runDir();

16
src/crash/CMakeLists.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
#pragma once
void qsCheckCrash(int argc, char** argv);

View file

@ -11,6 +11,7 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <unistd.h> #include <unistd.h>
#include "../../core/common.hpp"
#include "../../dbus/properties.hpp" #include "../../dbus/properties.hpp"
#include "dbus_watcher_interface.h" #include "dbus_watcher_interface.h"
#include "item.hpp" #include "item.hpp"
@ -31,7 +32,10 @@ StatusNotifierHost::StatusNotifierHost(QObject* parent): QObject(parent) {
return; return;
} }
this->hostId = QString("org.kde.StatusNotifierHost-") + QString::number(getpid()); this->hostId = QString("org.kde.StatusNotifierHost-%1-%2")
.arg(QString::number(getpid()))
.arg(QString::number(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch()));
auto success = bus.registerService(this->hostId); auto success = bus.registerService(this->hostId);
if (!success) { if (!success) {
@ -98,7 +102,7 @@ void StatusNotifierHost::connectToWatcher() {
[this](QStringList value, QDBusError error) { // NOLINT [this](QStringList value, QDBusError error) { // NOLINT
if (error.isValid()) { if (error.isValid()) {
qCWarning(logStatusNotifierHost).noquote() qCWarning(logStatusNotifierHost).noquote()
<< "Error reading \"RegisteredStatusNotifierITems\" property of watcher" << "Error reading \"RegisteredStatusNotifierItems\" property of watcher"
<< this->watcher->service(); << this->watcher->service();
qCWarning(logStatusNotifierHost) << error; qCWarning(logStatusNotifierHost) << error;