File indexing completed on 2024-04-28 09:21:02
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 }