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;