File indexing completed on 2024-12-08 10:52:35
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 <QCoreApplication> 0007 #include <QDebug> 0008 #include <QFile> 0009 #include <QJsonDocument> 0010 #include <QLibraryInfo> 0011 #include <QPluginLoader> 0012 #include <QProcess> 0013 #include <QScopeGuard> 0014 #include <QSettings> 0015 #include <QStandardPaths> 0016 0017 #include <cerrno> 0018 #include <chrono> 0019 #include <memory> 0020 #include <tuple> 0021 #include <utility> 0022 0023 #include <poll.h> 0024 #include <systemd/sd-daemon.h> 0025 #include <unistd.h> 0026 0027 #include "../coredump.h" 0028 #include "../metadata.h" 0029 #include "../socket.h" 0030 #include "DumpTruckInterface.h" 0031 0032 using namespace std::chrono_literals; 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::location(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 using ArgumentsPidTuple = std::tuple<QStringList, bool>; 0048 0049 static ArgumentsPidTuple metadataArguments(const Coredump &dump, const QString &metadataPath) 0050 { 0051 QStringList arguments; 0052 bool foundPID = false; 0053 0054 // Parse the metadata file. Ideally we'd should even stop passing a gazillion options 0055 // and instead rely on this file, then drkonqi 0056 // would also be in charge of removing it instead of us here. 0057 QSettings metadata(metadataPath, QSettings::IniFormat); 0058 metadata.beginGroup(QStringLiteral("KCrash")); 0059 const QStringList keys = metadata.allKeys(); 0060 for (const QString &key : keys) { 0061 const QString value = metadata.value(key).toString(); 0062 0063 if (key == QLatin1String("exe")) { 0064 if (value.endsWith(QStringLiteral("/drkonqi"))) { 0065 qWarning() << "drkonqi crashed, we aren't going to invoke it again, we might be the reason it crashd :O"; 0066 return {}; 0067 } 0068 if (value != dump.exe) { 0069 qWarning() << "the exe in the metadata file doesn't match the exe in the journal entry! aborting" << value << dump.exe; 0070 return {}; 0071 } 0072 // exe purely exists for our benefit, don't forward it to drkonqi. 0073 continue; 0074 } 0075 0076 if (key == QLatin1String("pid")) { 0077 foundPID = true; 0078 } 0079 0080 arguments << QStringLiteral("--%1").arg(key); 0081 if (value != QLatin1String("true") && value != QLatin1String("false")) { // not a bool value, append as arg 0082 arguments << value; 0083 } 0084 } 0085 metadata.endGroup(); 0086 0087 return {arguments, foundPID}; 0088 } 0089 0090 static ArgumentsPidTuple includeAllArguments(const Coredump &dump) 0091 { 0092 // Synthesize drkonqi arguments from the dump alone, this has limited functionality and is meant for use with 0093 // non-KDE apps. 0094 QStringList arguments; 0095 arguments << QStringLiteral("--signal") << QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_SIGNAL"))); 0096 arguments << QStringLiteral("--pid") << QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_PID"))); 0097 arguments << QStringLiteral("--restarted"); // Cannot restart foreign apps! 0098 return {arguments, true}; 0099 } 0100 0101 static ArgumentsPidTuple includeKWinWaylandArguments(const Coredump &dump) 0102 { 0103 auto [arguments, foundPID] = includeAllArguments(dump); 0104 arguments << QStringLiteral("--bugaddress") << QStringLiteral("submit@bugs.kde.org") << QStringLiteral("--appname") << dump.exe; 0105 return {arguments, foundPID}; 0106 } 0107 0108 static bool tryDrkonqi(const Coredump &dump) 0109 { 0110 const QString metadataPath = Metadata::resolveMetadataPath(dump.pid); 0111 const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("drkonqirc")); 0112 0113 // Arm removal. If we return early this will ensure clean up, otherwise we dismiss it later 0114 auto deleteFile = qScopeGuard([metadataPath] { 0115 QFile::remove(metadataPath); 0116 }); 0117 0118 if (qEnvironmentVariableIsSet("KDE_DEBUG")) { 0119 qWarning() << "KDE_DEBUG set. Not invoking DrKonqi."; 0120 return false; 0121 } 0122 0123 if (drkonqiExe().isEmpty()) { 0124 qWarning() << "Couldn't find drkonqi exe"; 0125 return false; 0126 } 0127 0128 QStringList arguments; 0129 bool foundPID = false; 0130 0131 if (!metadataPath.isEmpty()) { 0132 // A KDE app crash has metadata, build arguments from that 0133 std::tie(arguments, foundPID) = metadataArguments(dump, metadataPath); 0134 } else if (dump.exe.endsWith(QLatin1String("/kwin_wayland"))) { 0135 // When the compositor goes down it may not have time to store metadata, in that case we'll fake them. 0136 std::tie(arguments, foundPID) = includeKWinWaylandArguments(dump); 0137 } else if (QSettings(configFile, QSettings::IniFormat).value(QStringLiteral("IncludeAll")).toBool()) { 0138 // Handle non-KDE apps when in IncludeAll mode. 0139 std::tie(arguments, foundPID) = includeAllArguments(dump); 0140 } else { 0141 return false; 0142 } 0143 0144 if (arguments.isEmpty() || !foundPID) { 0145 // There is a chance that somehow the metadata file writing failed or is incomplete. Do some trivial 0146 // checks to catch and ignore such cases. Otherwise we risk drkonqi crashing due to internally failed 0147 // assertions. 0148 qWarning() << "Failed to read metadata from crash" << arguments << foundPID; 0149 return false; 0150 } 0151 0152 // Append Coredump data. This allow us to not have to talk to journald again on the drkonqi side. 0153 QSettings metadata(Metadata::metadataPath(dump.pid), QSettings::IniFormat); 0154 metadata.beginGroup(QStringLiteral("Journal")); 0155 for (auto it = dump.m_rawData.cbegin(); it != dump.m_rawData.cend(); ++it) { 0156 metadata.setValue(QString::fromUtf8(it.key()), it.value()); 0157 } 0158 metadata.endGroup(); 0159 metadata.sync(); 0160 0161 setenv("DRKONQI_BACKEND", "COREDUMPD", 1); 0162 setenv("DRKONQI_METADATA_FILE", qPrintable(metadataPath), 1); 0163 0164 deleteFile.dismiss(); // let drkonqi handle cleanup if we get here 0165 0166 // We must start drkonqi in a new slice. This launcher will want to terminate quickly and we enforce that 0167 // through maximum run time in the unit configuration. If drkonqi wasn't in a new slice it'd get killed with us. 0168 QProcess::execute(drkonqiExe(), arguments); 0169 0170 return true; // always considered handled, even if drkonqi crashes or something 0171 } 0172 0173 class DrKonqiTruck : public DumpTruckInterface 0174 { 0175 public: 0176 DrKonqiTruck() = default; 0177 ~DrKonqiTruck() override = default; 0178 0179 bool handle(const Coredump &dump) override 0180 { 0181 if (!QFile::exists(dump.filename)) { 0182 QFile::remove(Metadata::resolveMetadataPath(dump.pid)); // without trace we'll never run drkonqi as it can't file a bug anyway 0183 return false; 0184 } 0185 0186 return tryDrkonqi(dump); 0187 } 0188 0189 private: 0190 Q_DISABLE_COPY_MOVE(DrKonqiTruck) 0191 }; 0192 0193 static void onNewDump(const Coredump &dump) 0194 { 0195 static DrKonqiTruck drkonqi; 0196 if (drkonqi.handle(dump)) { 0197 return; 0198 } 0199 0200 if (qEnvironmentVariableIntValue("KDE_COREDUMP_NOTIFY") >= 1) { 0201 // Developers need to explicitly opt into the notifications. They 0202 // have no l10n and are also uniquely useless to users. 0203 static QPluginLoader loader(QStringLiteral("drkonqi/KDECoredumpNotifierTruck")); 0204 if (!loader.load()) { 0205 qWarning() << "failed to load" << loader.fileName() << loader.errorString(); 0206 return; 0207 } 0208 auto notifier = qobject_cast<DumpTruckInterface *>(loader.instance()); 0209 Q_ASSERT(notifier); 0210 if (notifier->handle(dump)) { 0211 return; 0212 } 0213 } 0214 0215 qWarning() << "Nothing handled the dump :O"; 0216 } 0217 0218 int main(int argc, char **argv) 0219 { 0220 QCoreApplication app(argc, argv); 0221 app.setApplicationName(QStringLiteral("drkonqi-coredump-launcher")); 0222 app.setOrganizationDomain(QStringLiteral("kde.org")); 0223 0224 if (sd_listen_fds(false) != 1) { 0225 qFatal("Not exactly one fd passed by systemd. Quel malheur!"); 0226 return 1; 0227 } 0228 0229 // Reading is very awkward and I'm not even sure why. 0230 // It seems like QLocalSocket doesn't manage to model stream sockets properly or at least not SOCK_SEQPACKET. 0231 // The socket on our end never notices that the remote has closed and even terminated already. 0232 // 0233 // Since we don't really need to do anything fancy we'll simply poll on our own instead of relying on QLS. 0234 0235 QByteArray json; 0236 QByteArray segment; 0237 segment.resize(Socket::DatagramSize); 0238 while (true) { 0239 struct pollfd poll { 0240 }; 0241 poll.fd = SD_LISTEN_FDS_START; 0242 poll.events = POLLIN; 0243 struct timespec time { 0244 }; 0245 time.tv_nsec = (2500ns).count(); 0246 ppoll(&poll, 1, &time, nullptr); 0247 0248 if (poll.revents & POLLERR) { 0249 qFatal("Socket had an error"); 0250 break; 0251 } 0252 0253 if (poll.revents & POLLNVAL) { 0254 qFatal("Socket was invalid"); 0255 break; 0256 } 0257 0258 if (poll.revents & POLLIN) { 0259 const size_t size = read(poll.fd, segment.data(), segment.size()); 0260 if (size == 0 && poll.revents & POLLHUP) { 0261 break; // zero read + POLLHUP = EOS says the manpage 0262 } 0263 json.append(segment.data(), size); 0264 } 0265 } 0266 close(SD_LISTEN_FDS_START); 0267 0268 QJsonParseError error{}; 0269 const QJsonDocument document = QJsonDocument::fromJson(json, &error); 0270 if (error.error != QJsonParseError::NoError) { 0271 qWarning() << "json parse error" << error.errorString(); 0272 return 1; 0273 } 0274 0275 // Unset a slew of systemd variables. 0276 unsetenv("JOURNAL_STREAM"); 0277 unsetenv("INVOCATION_ID"); 0278 unsetenv("LISTEN_FDNAMES"); 0279 unsetenv("LISTEN_FDS"); 0280 unsetenv("LISTEN_PID"); 0281 unsetenv("MANAGERPID"); 0282 0283 onNewDump(Coredump(document)); 0284 0285 return 0; 0286 }