File indexing completed on 2024-04-28 05:26:50

0001 /*
0002     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #include <poll.h>
0007 #include <systemd/sd-daemon.h>
0008 #include <unistd.h>
0009 
0010 #include <chrono>
0011 
0012 #include <QCoreApplication>
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QJsonDocument>
0017 #include <QLibraryInfo>
0018 #include <QPluginLoader>
0019 #include <QProcess>
0020 #include <QScopeGuard>
0021 #include <QStandardPaths>
0022 
0023 #include <KConfig>
0024 #include <KConfigGroup>
0025 
0026 #include "../coredump.h"
0027 #include "../metadata.h"
0028 #include "../socket.h"
0029 #include "DumpTruckInterface.h"
0030 
0031 using namespace std::chrono_literals;
0032 using namespace Qt::StringLiterals;
0033 
0034 static QString drkonqiExe()
0035 {
0036     // Borrowed from kcrash.cpp
0037     static QStringList paths = QFile::decodeName(qgetenv("LIBEXEC_PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts)
0038         + QStringList{
0039             QCoreApplication::applicationDirPath(), // then look where our application binary is located
0040             QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath), // look where libexec path is (can be set in qt.conf)
0041             QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR), // look at our installation location
0042         };
0043     static QString exec = QStandardPaths::findExecutable(QStringLiteral("drkonqi"), paths);
0044     return exec;
0045 }
0046 
0047 constexpr auto KCRASH_KEY = "kcrash"_L1;
0048 constexpr auto DRKONQI_KEY = "drkonqi"_L1;
0049 constexpr auto PICKED_UP_KEY = "PickedUp"_L1;
0050 
0051 [[nodiscard]] QJsonObject kcrashToDrKonqiMetadata(const Coredump &dump, const QString &kcrashMetadataPath)
0052 {
0053     auto contextObject = QJsonObject{{u"version"_s, 2}};
0054     {
0055         QJsonObject kcrashObject;
0056         KConfig kcrashMetadata(kcrashMetadataPath, KConfig::SimpleConfig);
0057         auto group = kcrashMetadata.group(u"KCrash"_s);
0058         const QStringList keys = group.keyList();
0059         for (const auto &key : keys) {
0060             const auto value = group.readEntry(key);
0061             kcrashObject.insert(key, value);
0062         }
0063         contextObject.insert(KCRASH_KEY, kcrashObject);
0064     }
0065     {
0066         QJsonObject journalObject;
0067         for (auto it = dump.m_rawData.cbegin(); it != dump.m_rawData.cend(); ++it) {
0068             journalObject.insert(QString::fromUtf8(it.key()), QString::fromUtf8(it.value()));
0069         }
0070         contextObject.insert(u"journal"_s, journalObject);
0071     }
0072     {
0073         contextObject.insert(DRKONQI_KEY, QJsonObject{{PICKED_UP_KEY, true}});
0074     }
0075 
0076     return contextObject;
0077 }
0078 
0079 [[nodiscard]] QJsonObject &synthesizeKCrashInto(const Coredump &dump, QJsonObject &metadata)
0080 {
0081     if (!metadata[KCRASH_KEY].toObject().isEmpty()) {
0082         return metadata; // already has data
0083     }
0084     if (!dump.exe.endsWith("/kwin_wayland"_L1)) {
0085         return metadata; // isn't kwin
0086     }
0087 
0088     auto object = metadata[KCRASH_KEY].toObject();
0089     object.insert(u"signal"_s, QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_SIGNAL"))));
0090     object.insert(u"pid"_s, QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_PID"))));
0091     object.insert(u"restarted"_s, true); // Cannot restart kwin_wayland. Autostarts if anything.
0092     object.insert(u"bugaddress"_s, u"submit@bugs.kde.org"_s);
0093     object.insert(u"appname"_s, dump.exe);
0094     metadata[KCRASH_KEY] = object;
0095 
0096     return metadata;
0097 }
0098 
0099 [[nodiscard]] QJsonObject &synthesizeGenericInto(const Coredump &dump, QJsonObject &metadata)
0100 {
0101     auto object = metadata[KCRASH_KEY].toObject();
0102 
0103     if (!object.isEmpty()) {
0104         return metadata;
0105     }
0106 
0107     static const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("drkonqirc"));
0108     if (!KConfig(configFile, KConfig::SimpleConfig).group(u"General"_s).readEntry(QStringLiteral("IncludeAll"), false)) {
0109         return metadata;
0110     }
0111 
0112     object.insert(u"signal"_s, QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_SIGNAL"))));
0113     object.insert(u"pid"_s, QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_PID"))));
0114     object.insert(u"restarted"_s, true); // Cannot restart foreign apps!
0115     metadata[KCRASH_KEY] = object;
0116 
0117     return metadata;
0118 }
0119 
0120 [[nodiscard]] QStringList metadataArguments(const QVariantHash &kcrash)
0121 {
0122     QStringList arguments;
0123 
0124     for (auto [key, valueVariant] : kcrash.asKeyValueRange()) {
0125         const auto value = valueVariant.toString();
0126 
0127         if (key == QLatin1String("exe")) {
0128             if (value.endsWith(QStringLiteral("/drkonqi"))) {
0129                 qWarning() << "drkonqi crashed, we aren't going to invoke it again, we might be the reason it crashed :O";
0130                 return {};
0131             }
0132             // exe purely exists for our benefit, don't forward it to drkonqi.
0133             continue;
0134         }
0135 
0136         arguments << QStringLiteral("--%1").arg(key);
0137         if (value != QLatin1String("true") && value != QLatin1String("false")) { // not a bool value, append as arg
0138             arguments << value;
0139         }
0140     }
0141 
0142     return arguments;
0143 }
0144 
0145 QJsonObject readFromDisk(const QString &drkonqiMetadataPath)
0146 {
0147     if (!QFile::exists(drkonqiMetadataPath)) {
0148         return {};
0149     }
0150 
0151     QFile file(drkonqiMetadataPath);
0152     if (!file.open(QFile::ReadOnly)) {
0153         qWarning() << "Failed to open for reading:" << drkonqiMetadataPath;
0154     }
0155 
0156     return QJsonDocument::fromJson(file.readAll()).object();
0157 }
0158 
0159 void writeToDisk(const QJsonObject &contextObject, const QString &drkonqiMetadataPath)
0160 {
0161     QDir().mkpath(QFileInfo(drkonqiMetadataPath).path());
0162 
0163     QFile file(drkonqiMetadataPath);
0164     if (file.open(QFile::WriteOnly | QFile::Truncate)) {
0165         file.write(QJsonDocument(contextObject).toJson());
0166     } else {
0167         qWarning() << "Failed to open for writing:" << drkonqiMetadataPath;
0168     }
0169 }
0170 
0171 bool isPickedUp(const QJsonObject &metadata)
0172 {
0173     return metadata[DRKONQI_KEY].toObject().value(PICKED_UP_KEY).toBool(false);
0174 }
0175 
0176 static bool tryDrkonqi(const Coredump &dump)
0177 {
0178     const QString kcrashMetadataPath = Metadata::resolveKCrashMetadataPath(dump.exe, dump.bootId, dump.pid);
0179     // Arm removal. In all cases we'll want to remove the kcrash metadata (we possibly created expanded drkonqi metadata instead)
0180     auto deleteFile = qScopeGuard([kcrashMetadataPath] {
0181         if (!kcrashMetadataPath.isEmpty()) { // don't warn about null path
0182             QFile::remove(kcrashMetadataPath);
0183         }
0184     });
0185 
0186     const QString drkonqiMetadataPath = Metadata::drkonqiMetadataPath(dump.exe, dump.bootId, dump.timestamp, dump.pid);
0187 
0188     QJsonObject metadata = readFromDisk(drkonqiMetadataPath);
0189     if (isPickedUp(metadata)) {
0190         return true; // already handled previously
0191     }
0192     if (metadata.isEmpty()) {
0193         // if our metadata doesn't exist yet try to pick it up from kcrash
0194         metadata = kcrashToDrKonqiMetadata(dump, kcrashMetadataPath);
0195         // or synthesize it
0196         metadata = synthesizeKCrashInto(dump, metadata);
0197         // or force-handle the crash
0198         metadata = synthesizeGenericInto(dump, metadata);
0199         writeToDisk(metadata, drkonqiMetadataPath);
0200     }
0201 
0202     if (metadata.isEmpty() || metadata[KCRASH_KEY].toObject().isEmpty()) {
0203         return false; // no metadata, or no kcrash metadata -> don't know what to do with this
0204     }
0205 
0206     if (!QFile::exists(dump.filename)) {
0207         return false; // no trace -> nothing to handle
0208     }
0209 
0210     if (qEnvironmentVariableIsSet("KDE_DEBUG")) {
0211         qWarning() << "KDE_DEBUG set. Not invoking DrKonqi.";
0212         return false;
0213     }
0214 
0215     if (drkonqiExe().isEmpty()) {
0216         qWarning() << "Couldn't find drkonqi exe";
0217         return false;
0218     }
0219 
0220     setenv("DRKONQI_BACKEND", "COREDUMPD", 1);
0221     setenv("DRKONQI_METADATA_FILE", qPrintable(drkonqiMetadataPath), 1);
0222 
0223     // We must start drkonqi in a new slice. This launcher will want to terminate quickly and we enforce that
0224     // through maximum run time in the unit configuration. If drkonqi wasn't in a new slice it'd get killed with us.
0225     QProcess::execute(drkonqiExe(), metadataArguments(metadata[KCRASH_KEY].toObject().toVariantHash()));
0226 
0227     return true; // always considered handled, even if drkonqi crashes or something
0228 }
0229 
0230 class DrKonqiTruck : public DumpTruckInterface
0231 {
0232 public:
0233     DrKonqiTruck() = default;
0234     ~DrKonqiTruck() override = default;
0235 
0236     bool handle(const Coredump &dump) override
0237     {
0238         return tryDrkonqi(dump);
0239     }
0240 
0241 private:
0242     Q_DISABLE_COPY_MOVE(DrKonqiTruck)
0243 };
0244 
0245 static void onNewDump(const Coredump &dump)
0246 {
0247     static DrKonqiTruck drkonqi;
0248     if (drkonqi.handle(dump)) {
0249         return;
0250     }
0251 
0252     if (qEnvironmentVariableIntValue("KDE_COREDUMP_NOTIFY") >= 1) {
0253         // Developers need to explicitly opt into the notifications. They
0254         // have no l10n and are also uniquely useless to users.
0255         static QPluginLoader loader(QStringLiteral("drkonqi/KDECoredumpNotifierTruck"));
0256         if (!loader.load()) {
0257             qWarning() << "failed to load" << loader.fileName() << loader.errorString();
0258             return;
0259         }
0260         auto notifier = qobject_cast<DumpTruckInterface *>(loader.instance());
0261         Q_ASSERT(notifier);
0262         if (notifier->handle(dump)) {
0263             return;
0264         }
0265     }
0266 
0267     qWarning() << "Nothing handled the dump :O";
0268 }
0269 
0270 int main(int argc, char **argv)
0271 {
0272     QCoreApplication app(argc, argv);
0273     app.setApplicationName(QStringLiteral("drkonqi-coredump-launcher"));
0274     app.setOrganizationDomain(QStringLiteral("kde.org"));
0275 
0276     if (sd_listen_fds(false) != 1) {
0277         qFatal("Not exactly one fd passed by systemd. Quel malheur!");
0278         return 1;
0279     }
0280 
0281     // Reading is very awkward and I'm not even sure why.
0282     // It seems like QLocalSocket doesn't manage to model stream sockets properly or at least not SOCK_SEQPACKET.
0283     // The socket on our end never notices that the remote has closed and even terminated already.
0284     //
0285     // Since we don't really need to do anything fancy we'll simply poll on our own instead of relying on QLS.
0286 
0287     QByteArray json;
0288     QByteArray segment;
0289     segment.resize(Socket::DatagramSize);
0290     while (true) {
0291         struct pollfd poll {
0292         };
0293         poll.fd = SD_LISTEN_FDS_START;
0294         poll.events = POLLIN;
0295         struct timespec time {
0296         };
0297         time.tv_nsec = (2500ns).count();
0298         ppoll(&poll, 1, &time, nullptr);
0299 
0300         if (poll.revents & POLLERR) {
0301             qFatal("Socket had an error");
0302             break;
0303         }
0304 
0305         if (poll.revents & POLLNVAL) {
0306             qFatal("Socket was invalid");
0307             break;
0308         }
0309 
0310         if (poll.revents & POLLIN) {
0311             const size_t size = read(poll.fd, segment.data(), segment.size());
0312             if (size == 0 && poll.revents & POLLHUP) {
0313                 break; // zero read + POLLHUP = EOS says the manpage
0314             }
0315             json.append(segment.data(), size);
0316         }
0317     }
0318     close(SD_LISTEN_FDS_START);
0319 
0320     QJsonParseError error{};
0321     const QJsonDocument document = QJsonDocument::fromJson(json, &error);
0322     if (error.error != QJsonParseError::NoError) {
0323         qWarning() << "json parse error" << error.errorString();
0324         return 1;
0325     }
0326 
0327     // Unset a slew of systemd variables.
0328     unsetenv("JOURNAL_STREAM");
0329     unsetenv("INVOCATION_ID");
0330     unsetenv("LISTEN_FDNAMES");
0331     unsetenv("LISTEN_FDS");
0332     unsetenv("LISTEN_PID");
0333     unsetenv("MANAGERPID");
0334 
0335     onNewDump(Coredump(document));
0336 
0337     return 0;
0338 }