diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..0086358d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml
new file mode 100644
index 00000000..13dcd33d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -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: " General information
+
+
+ ```
+
+
+
+ ```
+
+
+ "
+ 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 `.
+ 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 ` 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.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b55c751f..e3c01592 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF)
option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang
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(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
@@ -29,6 +30,7 @@ option(SERVICE_UPOWER "UPower service" ON)
option(SERVICE_NOTIFICATIONS "Notification server" ON)
message(STATUS "Quickshell configuration")
+message(STATUS " Crash reporter: ${CRASH_REPORTER}")
message(STATUS " Jemalloc: ${USE_JEMALLOC}")
message(STATUS " Build tests: ${BUILD_TESTING}")
message(STATUS " Sockets: ${SOCKETS}")
diff --git a/default.nix b/default.nix
index 34cc0f4b..1ddb99b5 100644
--- a/default.nix
+++ b/default.nix
@@ -9,6 +9,7 @@
ninja,
qt6,
cli11,
+ breakpad,
jemalloc,
wayland,
wayland-protocols,
@@ -28,6 +29,7 @@
else "unknown"),
debug ? false,
+ withCrashReporter ? true,
withJemalloc ? true, # masks heap fragmentation
withQtSvg ? true,
withWayland ? true,
@@ -55,6 +57,7 @@
qt6.qtdeclarative
cli11
]
+ ++ (lib.optional withCrashReporter breakpad)
++ (lib.optional withJemalloc jemalloc)
++ (lib.optional withQtSvg qt6.qtsvg)
++ (lib.optionals withWayland [ qt6.qtwayland wayland ])
@@ -67,6 +70,7 @@
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ]
+ ++ lib.optional (!withCrashReporter) "-DCRASH_REPORTER=OFF"
++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF"
++ lib.optional (!withWayland) "-DWAYLAND=OFF"
++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index be3adaf8..42954775 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,6 +5,10 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(core)
add_subdirectory(io)
+if (CRASH_REPORTER)
+ add_subdirectory(crash)
+endif()
+
if (DBUS)
add_subdirectory(dbus)
endif()
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index fb39287c..0d6f7211 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -41,9 +41,19 @@ qt_add_library(quickshell-core STATIC
clock.cpp
logging.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)
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11)
diff --git a/src/core/build.hpp.in b/src/core/build.hpp.in
new file mode 100644
index 00000000..ecf5dfc4
--- /dev/null
+++ b/src/core/build.hpp.in
@@ -0,0 +1,6 @@
+#pragma once
+
+// NOLINTBEGIN
+#define GIT_REVISION "@GIT_REVISION@"
+#define CRASH_REPORTER @CRASH_REPORTER_DEF@
+// NOLINTEND
diff --git a/src/core/common.cpp b/src/core/common.cpp
new file mode 100644
index 00000000..d09661f1
--- /dev/null
+++ b/src/core/common.cpp
@@ -0,0 +1,9 @@
+#include "common.hpp"
+
+#include
+
+namespace qs {
+
+const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
+
+}
diff --git a/src/core/common.hpp b/src/core/common.hpp
new file mode 100644
index 00000000..36094f89
--- /dev/null
+++ b/src/core/common.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+namespace qs {
+
+struct Common {
+ static const QDateTime LAUNCH_TIME;
+};
+
+} // namespace qs
diff --git a/src/core/crashinfo.cpp b/src/core/crashinfo.cpp
new file mode 100644
index 00000000..f441530f
--- /dev/null
+++ b/src/core/crashinfo.cpp
@@ -0,0 +1,19 @@
+#include "crashinfo.hpp"
+
+#include
+
+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
+
+}
diff --git a/src/core/crashinfo.hpp b/src/core/crashinfo.hpp
new file mode 100644
index 00000000..a867563f
--- /dev/null
+++ b/src/core/crashinfo.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+#include
+
+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
diff --git a/src/core/logging.cpp b/src/core/logging.cpp
index 99899aa7..887e145f 100644
--- a/src/core/logging.cpp
+++ b/src/core/logging.cpp
@@ -24,6 +24,7 @@
#include
#include
+#include "crashinfo.hpp"
#include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp"
@@ -198,14 +199,16 @@ void ThreadLogging::init() {
if (logMfd != -1) {
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);
}
if (dlogMfd != -1) {
+ crash::CrashInfo::INSTANCE.logFd = dlogMfd;
+
this->detailedFile = new QFile();
// 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);
if (!this->detailedWriter.writeHeader()) {
@@ -245,7 +248,7 @@ void ThreadLogging::initFs() {
auto* file = new QFile(path);
auto* detailedFile = new QFile(detailedPath);
- if (!file->open(QFile::WriteOnly | QFile::Truncate)) {
+ if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
@@ -256,7 +259,7 @@ void ThreadLogging::initFs() {
}
// buffered by WriteBuffer
- if (!detailedFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered)) {
+ if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath;
@@ -287,6 +290,8 @@ void ThreadLogging::initFs() {
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
}
+ crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
+
this->detailedFile = detailedFile;
this->detailedWriter.setDevice(detailedFile);
diff --git a/src/core/main.cpp b/src/core/main.cpp
index e48213ac..25257b5f 100644
--- a/src/core/main.cpp
+++ b/src/core/main.cpp
@@ -1,13 +1,17 @@
#include "main.hpp"
+#include
#include
#include
#include
#include // NOLINT: Need to include this for impls of some CLI11 classes
+#include
#include
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -24,162 +28,164 @@
#include
#include
+#include "build.hpp"
+#include "common.hpp"
+#include "crashinfo.hpp"
#include "logging.hpp"
#include "paths.hpp"
#include "plugin.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;
- auto* qArgV = argv;
+void processCommand(int argc, char** argv, CommandInfo& info) {
+ 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 configFilePath;
- QString shellId;
- auto printInfo = false;
+ QString& operator*() { return this->str; }
- auto debugPort = -1;
- auto waitForDebug = false;
+ private:
+ QString str;
+ };
- auto useQApplication = false;
- auto nativeTextRendering = false;
- auto desktopSettingsAware = true;
- QHash envOverrides;
+ class QStringRefOption {
+ public:
+ QStringRefOption(QString* str): str(str) {}
+ QStringRefOption& operator=(const std::string& str) {
+ *this->str = QString::fromStdString(str);
+ return *this;
+ }
- {
- auto app = CLI::App("");
+ private:
+ QString* str;
+ };
- class QStringOption {
- public:
- QStringOption() = default;
- QStringOption& operator=(const std::string& str) {
- this->str = QString::fromStdString(str);
- return *this;
- }
+ /// ---
+ QStringRefOption path(&info.configPath);
+ QStringRefOption manifest(&info.manifestPath);
+ QStringRefOption config(&info.configName);
+ QStringRefOption workdirRef(&info.initialWorkdir);
- 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:
- QString str;
- };
+ auto* pathArg =
+ selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)");
- class QStringRefOption {
- public:
- QStringRefOption(QString* str): str(str) {}
- QStringRefOption& operator=(const std::string& str) {
- *this->str = QString::fromStdString(str);
- return *this;
- }
+ auto* mfArg = selection->add_option(
+ "-m,--manifest",
+ manifest,
+ "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n"
+ "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)"
+ );
- private:
- QString* str;
- };
+ auto* cfgArg = selection->add_option(
+ "-c,--config",
+ config,
+ "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)"
+ );
- /// ---
- QStringOption path;
- QStringOption manifest;
- QStringOption config;
- QStringRefOption workdirRef(&workingDirectory);
+ selection->add_option("-d,--workdir", workdirRef, "Initial working directory.");
- auto* selection = app.add_option_group(
- "Config Selection",
- "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)"
- );
+ pathArg->excludes(mfArg, cfgArg);
- 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(
- "-m,--manifest",
- manifest,
- "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n"
- "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)"
- );
+ auto* debugPortArg = debug
+ ->add_option(
+ "--debugport",
+ info.debugPort,
+ "Open the given port for a QML debugger to connect to."
+ )
+ ->check(CLI::Range(0, 65535));
- auto* cfgArg = selection->add_option(
- "-c,--config",
- config,
- "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)"
- );
+ debug
+ ->add_flag(
+ "--waitfordebug",
+ 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
- ->add_option(
- "--debugport",
- debugPort,
- "Open the given port for a QML debugger to connect to."
- )
- ->check(CLI::Range(0, 65535));
+ auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file.");
+ readLog->add_option("path", logPath, "Path to the log file to read")->required();
- debug
- ->add_flag(
- "--waitfordebug",
- waitForDebug,
- "Wait for a debugger to attach to the given port before launching."
- )
- ->needs(debugPortArg);
+ readLog->add_option(
+ "-f,--filter",
+ logFilter,
+ "Logging categories to display. (same syntax as QT_LOGGING_RULES)"
+ );
- /// ---
- auto sparseLogsOnly = false;
- 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.");
+ readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages.");
+ readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)");
- app.add_flag(
- "--no-detailed-logs",
- sparseLogsOnly,
- "Do not enable this unless you know what you are doing."
- );
+ try {
+ app.parse(argc, argv);
+ } catch (const CLI::ParseError& e) {
+ exit(app.exit(e)); // NOLINT
+ };
- /// ---
- QStringOption logPath;
- QStringOption logFilter;
- auto logNoTime = false;
-
- auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file.");
- readLog->add_option("path", logPath, "Path to the log file to read")->required();
-
- 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;
+ if (*printVersion) {
+ std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl;
+ exit(0); // NOLINT
+ } else if (*readLog) {
+ auto file = QFile(*logPath);
+ if (!file.open(QFile::ReadOnly)) {
+ qCritical() << "Failed to open log for reading:" << *logPath;
+ exit(-1); // NOLINT
} 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) \
QString name = expr; \
if (rname.isEmpty() && !name.isEmpty()) { \
@@ -189,231 +195,341 @@ int qs_main(int argc, char** argv) {
}
#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString())
- // NOLINTEND
+ // NOLINTEND
- QString basePath;
- int basePathLevel = 0;
- Q_UNUSED(basePathLevel);
- {
- // NOLINTBEGIN
- // clang-format off
- CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
- CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
- // clang-format on
- // NOLINTEND
+ QString basePath;
+ int basePathLevel = 0;
+ Q_UNUSED(basePathLevel);
+ {
+ // NOLINTBEGIN
+ // clang-format off
+ CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
+ CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
+ // clang-format on
+ // NOLINTEND
- if (printInfo) {
- // clang-format off
- std::cout << "Base path: " << OPTSTR(basePath) << "\n";
- std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
- std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
- // clang-format on
- }
- }
- foundbase:;
+ if (printInfo) {
+ // clang-format off
+ std::cout << "Base path: " << OPTSTR(basePath) << "\n";
+ std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
+ std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
+ // clang-format on
+ }
+ }
+foundbase:;
- QString configPath;
- int configPathLevel = 10;
- {
- // NOLINTBEGIN
- CHECK(configPath, optionConfigPath, 0, foundpath, *path);
- CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
- // NOLINTEND
+ QString configPath;
+ int configPathLevel = 10;
+ {
+ // NOLINTBEGIN
+ CHECK(configPath, optionConfigPath, 0, foundpath, path);
+ CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
+ // NOLINTEND
- if (printInfo) {
- // clang-format off
- std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
- std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
- std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
- // clang-format on
- }
- }
- foundpath:;
+ if (printInfo) {
+ // clang-format off
+ std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
+ std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
+ std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
+ // clang-format on
+ }
+ }
+foundpath:;
- QString manifestPath;
- int manifestPathLevel = 10;
- {
- // NOLINTBEGIN
- // clang-format off
- CHECK(manifestPath, optionManifestPath, 0, foundmf, *manifest);
- CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
- CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
- // clang-format on
- // NOLINTEND
+ QString manifestPath;
+ int manifestPathLevel = 10;
+ {
+ // NOLINTBEGIN
+ // clang-format off
+ CHECK(manifestPath, optionManifestPath, 0, foundmf, manifest);
+ CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
+ CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
+ // clang-format on
+ // NOLINTEND
- if (printInfo) {
- // clang-format off
- std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
- std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
- std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
- std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
- // clang-format on
- }
- }
- foundmf:;
+ if (printInfo) {
+ // clang-format off
+ std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
+ std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
+ std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
+ std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
+ // clang-format on
+ }
+ }
+foundmf:;
- QString configName;
- int configNameLevel = 10;
- {
- // NOLINTBEGIN
- CHECK(configName, optionConfigName, 0, foundname, *config);
- CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
- // NOLINTEND
+ QString configName;
+ int configNameLevel = 10;
+ {
+ // NOLINTBEGIN
+ CHECK(configName, optionConfigName, 0, foundname, config);
+ CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
+ // NOLINTEND
- if (printInfo) {
- // clang-format off
- std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
- std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
- std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
- // clang-format on
- }
- }
- foundname:;
+ if (printInfo) {
+ // clang-format off
+ std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
+ std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
+ std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
+ // clang-format on
+ }
+ }
+foundname:;
- if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
- 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;
+ QString configFilePath;
- auto split = line.split('=');
- if (split.length() != 2) {
- qCritical() << "manifest line not in expected format 'name = relativepath':"
- << line;
- return -1;
- }
+ if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
+ 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;
- if (split[0].trimmed() == configName) {
- configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
- goto haspath; // NOLINT
- }
- }
+ auto split = line.split('=');
+ if (split.length() != 2) {
+ qCritical() << "manifest line not in expected format 'name = relativepath':" << line;
+ exit(-1); // NOLINT
+ }
- qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
- return -1;
- } else if (manifestPathLevel < 2) {
- qCritical() << "cannot open config manifest at" << manifestPath;
- return -1;
+ if (split[0].trimmed() == configName) {
+ configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
+ goto foundp;
}
}
- {
- auto basePathInfo = QFileInfo(basePath);
- if (!basePathInfo.exists()) {
- qCritical() << "base path does not exist:" << basePath;
- return -1;
- } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
- qCritical() << "base path is not a directory" << basePath;
- return -1;
- }
+ qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
+ exit(-1); // NOLINT
+ } else if (manifestPathLevel < 2) {
+ qCritical() << "cannot open config manifest at" << manifestPath;
+ exit(-1); // NOLINT
+ }
+ }
- auto dir = QDir(basePath);
- for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
- if (entry == configName) {
- configFilePath = dir.filePath(entry);
- goto haspath; // NOLINT
- }
- }
+ {
+ auto basePathInfo = QFileInfo(basePath);
+ if (!basePathInfo.exists()) {
+ qCritical() << "base path does not exist:" << basePath;
+ 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;
- return -1;
+ auto dir = QDir(basePath);
+ 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);
- if (!configFile.exists()) {
- qCritical() << "config path does not exist:" << configFilePath;
- return -1;
- }
+ qCritical() << "no directory named " << configName << "found in base path" << basePath;
+ exit(-1); // NOLINT
+ }
+ } else {
+ configFilePath = basePath;
+ }
- if (configFile.isDir()) {
- configFilePath = QDir(configFilePath).filePath("shell.qml");
- }
+foundp:;
+ auto configFile = QFileInfo(configFilePath);
+ if (!configFile.exists()) {
+ qCritical() << "config path does not exist:" << configFilePath;
+ exit(-1); // NOLINT
+ }
- configFile = QFileInfo(configFilePath);
- if (!configFile.exists()) {
- qCritical() << "no shell.qml found in config path:" << configFilePath;
- return -1;
- } else if (configFile.isDir()) {
- qCritical() << "shell.qml is a directory:" << configFilePath;
- return -1;
- }
+ if (configFile.isDir()) {
+ configFilePath = QDir(configFilePath).filePath("shell.qml");
+ }
- configFilePath = QFileInfo(configFilePath).canonicalFilePath();
- configFile = QFileInfo(configFilePath);
- if (!configFile.exists()) {
- qCritical() << "config file does not exist:" << configFilePath;
- return -1;
- } else if (configFile.isDir()) {
- qCritical() << "config file is a directory:" << configFilePath;
- return -1;
- }
+ configFile = QFileInfo(configFilePath);
+ if (!configFile.exists()) {
+ qCritical() << "no shell.qml found in config path:" << configFilePath;
+ exit(-1); // NOLINT
+ } else if (configFile.isDir()) {
+ qCritical() << "shell.qml is a directory:" << configFilePath;
+ exit(-1); // NOLINT
+ }
+
+ 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 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()) {
- qCritical() << "config file does not exist";
- return -1;
+ auto qArgC = 1;
+ auto* qArgV = argv;
+
+ 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 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);
- if (!file.open(QFile::ReadOnly | QFile::Text)) {
- qCritical() << "could not open config file";
- return -1;
- }
+ crashHandler.init();
+ } else
+#endif
+ {
- auto stream = QTextStream(&file);
- while (!stream.atEnd()) {
- auto line = stream.readLine().trimmed();
- if (line.startsWith("//@ pragma ")) {
- auto pragma = line.sliced(11).trimmed();
+ auto command = CommandInfo {
+ .initialWorkdir = initialWorkdir,
+ .debugPort = debugPort,
+ .waitForDebug = waitForDebug,
+ .printInfo = printInfo,
+ .noColor = noColor,
+ .sparseLogsOnly = sparseLogsOnly,
+ };
- 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('=');
+ processCommand(argc, argv, command);
- if (splitIdx == -1) {
- qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
- return -1;
- }
+ // Start log manager - has to happen with an active event loop or offthread can't be started.
+ LogManager::init(!noColor, sparseLogsOnly);
- 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;
+#if CRASH_REPORTER
+ // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but
+ // this can be moved if it happens.
+ crashHandler.init();
+#endif
+
+ 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;
}
- } 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;
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()) {
qputenv(var.toUtf8(), val.toUtf8());
}
@@ -484,8 +600,8 @@ int qs_main(int argc, char** argv) {
QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait);
}
- if (!workingDirectory.isEmpty()) {
- QDir::setCurrent(workingDirectory);
+ if (!initialWorkdir.isEmpty()) {
+ QDir::setCurrent(initialWorkdir);
}
QuickshellPlugin::initPlugins();
diff --git a/src/core/paths.cpp b/src/core/paths.cpp
index 7e05530d..8f63b3aa 100644
--- a/src/core/paths.cpp
+++ b/src/core/paths.cpp
@@ -9,6 +9,8 @@
#include
#include
+#include "common.hpp"
+
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
QsPaths* QsPaths::instance() {
@@ -18,6 +20,15 @@ QsPaths* QsPaths::instance() {
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() {
if (this->cacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
@@ -73,7 +84,7 @@ QDir* QsPaths::instanceRunDir() {
this->instanceRunState = DirState::Failed;
} else {
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();
diff --git a/src/core/paths.hpp b/src/core/paths.hpp
index b2a1c193..9716e299 100644
--- a/src/core/paths.hpp
+++ b/src/core/paths.hpp
@@ -1,10 +1,12 @@
#pragma once
+#include
#include
class QsPaths {
public:
static QsPaths* instance();
static void init(QString shellId);
+ static QDir crashDir(const QString& shellId, const QDateTime& launchTime);
QDir* cacheDir();
QDir* runDir();
diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt
new file mode 100644
index 00000000..522b5b02
--- /dev/null
+++ b/src/crash/CMakeLists.txt
@@ -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)
diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp
new file mode 100644
index 00000000..dea6192c
--- /dev/null
+++ b/src/crash/handler.cpp
@@ -0,0 +1,180 @@
+#include "handler.hpp"
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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(context);
+
+ auto exe = std::array();
+ 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 {exe.data(), nullptr};
+
+ auto env = std::array();
+ auto envi = 0;
+
+ auto infoFd = dup(self->infoFd);
+ auto infoFdStr = std::array();
+ 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();
+ 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();
+ auto logFdStr = std::array();
+
+ 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
diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp
new file mode 100644
index 00000000..de7b46bc
--- /dev/null
+++ b/src/crash/handler.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include
+
+#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
diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp
new file mode 100644
index 00000000..3d296580
--- /dev/null
+++ b/src/crash/interface.cpp
@@ -0,0 +1,97 @@
+#include "interface.hpp"
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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(
+ "Quickshell has crashed. Please submit a bug report to help us fix it.",
+ 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(); }
diff --git a/src/crash/interface.hpp b/src/crash/interface.hpp
new file mode 100644
index 00000000..d7800435
--- /dev/null
+++ b/src/crash/interface.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+
+class CrashReporterGui: public QWidget {
+public:
+ CrashReporterGui(QString reportFolder, int pid);
+
+private slots:
+ void openFolder();
+
+ static void openReportUrl();
+ static void cancel();
+
+private:
+ QString reportFolder;
+};
diff --git a/src/crash/main.cpp b/src/crash/main.cpp
new file mode 100644
index 00000000..52776190
--- /dev/null
+++ b/src/crash/main.cpp
@@ -0,0 +1,165 @@
+#include "main.hpp"
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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.";
+}
diff --git a/src/crash/main.hpp b/src/crash/main.hpp
new file mode 100644
index 00000000..b6a282cf
--- /dev/null
+++ b/src/crash/main.hpp
@@ -0,0 +1,3 @@
+#pragma once
+
+void qsCheckCrash(int argc, char** argv);
diff --git a/src/services/status_notifier/host.cpp b/src/services/status_notifier/host.cpp
index 470b86a7..5fa9af0e 100644
--- a/src/services/status_notifier/host.cpp
+++ b/src/services/status_notifier/host.cpp
@@ -11,6 +11,7 @@
#include
#include
+#include "../../core/common.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_watcher_interface.h"
#include "item.hpp"
@@ -31,7 +32,10 @@ StatusNotifierHost::StatusNotifierHost(QObject* parent): QObject(parent) {
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);
if (!success) {
@@ -98,7 +102,7 @@ void StatusNotifierHost::connectToWatcher() {
[this](QStringList value, QDBusError error) { // NOLINT
if (error.isValid()) {
qCWarning(logStatusNotifierHost).noquote()
- << "Error reading \"RegisteredStatusNotifierITems\" property of watcher"
+ << "Error reading \"RegisteredStatusNotifierItems\" property of watcher"
<< this->watcher->service();
qCWarning(logStatusNotifierHost) << error;