File indexing completed on 2024-04-21 16:12:22

0001 /*
0002     SPDX-FileCopyrightText: 2000-2003 Hans Petter Bieker <bieker@kde.org>
0003     SPDX-FileCopyrightText: 2009 George Kiagiadakis <gkiagia@users.sourceforge.net>
0004     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 #include "drkonqi.h"
0009 #include "drkonqi_debug.h"
0010 
0011 #include <chrono>
0012 
0013 #include <QFileDialog>
0014 #include <QPointer>
0015 #include <QTemporaryFile>
0016 #include <QTextStream>
0017 #include <QTimerEvent>
0018 
0019 #include <KCrash>
0020 #include <KJobWidgets>
0021 #include <KLocalizedString>
0022 #include <KMessageBox>
0023 #include <QApplication>
0024 #include <kio/filecopyjob.h>
0025 
0026 #include "backtracegenerator.h"
0027 #include "crashedapplication.h"
0028 #include "debuggermanager.h"
0029 #include "drkonqibackends.h"
0030 #include "systeminformation.h"
0031 
0032 #ifdef SYSTEMD_AVAILABLE
0033 #include "coredumpbackend.h"
0034 #endif
0035 
0036 using namespace std::chrono_literals;
0037 
0038 static AbstractDrKonqiBackend *factorizeBackend()
0039 {
0040     // This is controlled by the environment because doing it as a cmdline option is supremely horrible because
0041     // DrKonqi is a singleton that gets created at a random point in time, while options are only set on it afterwards.
0042     // Since we don't want a nullptr backend we'll need the backend factorization to be independent of the cmdline.
0043     // This could maybe be changed but would require substantial rejiggering of the singleton to not have points in
0044     // time where there is no backend behind it.
0045 #ifdef SYSTEMD_AVAILABLE
0046     if (qgetenv("DRKONQI_BACKEND") == QByteArrayLiteral("COREDUMPD")) {
0047         qunsetenv("DRKONQI_BACKEND");
0048         return new CoredumpBackend;
0049     }
0050 #endif
0051     return new KCrashBackend();
0052 }
0053 
0054 DrKonqi::DrKonqi()
0055     : m_systemInformation(new SystemInformation())
0056     , m_backend(factorizeBackend())
0057     , m_signal(0)
0058     , m_pid(0)
0059     , m_kdeinit(false)
0060     , m_safer(false)
0061     , m_restarted(false)
0062     , m_keepRunning(false)
0063     , m_thread(0)
0064 {
0065 }
0066 
0067 DrKonqi::~DrKonqi()
0068 {
0069     delete m_systemInformation;
0070     delete m_backend;
0071 }
0072 
0073 // static
0074 DrKonqi *DrKonqi::instance()
0075 {
0076     static DrKonqi drKonqiInstance;
0077     return &drKonqiInstance;
0078 }
0079 
0080 // based on KCrashDelaySetHandler from kdeui/util/kcrash.cpp
0081 class EnableCrashCatchingDelayed : public QObject
0082 {
0083 public:
0084     EnableCrashCatchingDelayed()
0085     {
0086         startTimer(10s);
0087     }
0088 
0089 protected:
0090     void timerEvent(QTimerEvent *event) override
0091     {
0092         qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching";
0093         KCrash::setDrKonqiEnabled(true);
0094         killTimer(event->timerId());
0095         this->deleteLater();
0096     }
0097 };
0098 
0099 bool DrKonqi::init()
0100 {
0101     if (!instance()->m_backend->init()) {
0102         return false;
0103     } else { // all ok, continue initialization
0104         // Set drkonqi to handle its own crashes, but only if the crashed app
0105         // is not drkonqi already. If it is drkonqi, delay enabling crash catching
0106         // to prevent recursive crashes (in case it crashes at startup)
0107         if (crashedApplication()->fakeExecutableBaseName() != QLatin1String("drkonqi")) {
0108             qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching";
0109             KCrash::setDrKonqiEnabled(true);
0110         } else {
0111             new EnableCrashCatchingDelayed;
0112         }
0113         return true;
0114     }
0115 }
0116 
0117 // static
0118 SystemInformation *DrKonqi::systemInformation()
0119 {
0120     return instance()->m_systemInformation;
0121 }
0122 
0123 // static
0124 DebuggerManager *DrKonqi::debuggerManager()
0125 {
0126     return instance()->m_backend->debuggerManager();
0127 }
0128 
0129 // static
0130 CrashedApplication *DrKonqi::crashedApplication()
0131 {
0132     return instance()->m_backend->crashedApplication();
0133 }
0134 
0135 // static
0136 void DrKonqi::saveReport(const QString &reportText, QWidget *parent)
0137 {
0138     if (isSafer()) {
0139         QTemporaryFile tf;
0140         tf.setFileTemplate(QStringLiteral("XXXXXX.kcrash"));
0141         tf.setAutoRemove(false);
0142 
0143         if (tf.open()) {
0144             QTextStream textStream(&tf);
0145             textStream << reportText;
0146             textStream.flush();
0147             KMessageBox::information(parent, xi18nc("@info", "Report saved to <filename>%1</filename>.", tf.fileName()));
0148         } else {
0149             KMessageBox::error(parent, i18nc("@info", "Could not create a file in which to save the report."));
0150         }
0151     } else {
0152         QString defname = getSuggestedKCrashFilename(crashedApplication());
0153 
0154         QPointer<QFileDialog> dlg(new QFileDialog(parent, defname));
0155         dlg->selectFile(defname);
0156         dlg->setWindowTitle(i18nc("@title:window", "Select Filename"));
0157         dlg->setAcceptMode(QFileDialog::AcceptSave);
0158         dlg->setFileMode(QFileDialog::AnyFile);
0159         dlg->setOption(QFileDialog::DontResolveSymlinks, false);
0160         if (dlg->exec() != QDialog::Accepted) {
0161             return;
0162         }
0163 
0164         if (!dlg) {
0165             // Dialog is invalid, it was probably deleted (ex. via DBus call)
0166             // return and do not crash
0167             return;
0168         }
0169 
0170         QUrl fileUrl;
0171         if (!dlg->selectedUrls().isEmpty())
0172             fileUrl = dlg->selectedUrls().first();
0173         delete dlg;
0174 
0175         if (fileUrl.isValid()) {
0176             QTemporaryFile tf;
0177             if (tf.open()) {
0178                 QTextStream ts(&tf);
0179                 ts << reportText;
0180                 ts.flush();
0181             } else {
0182                 KMessageBox::error(parent,
0183                                    xi18nc("@info",
0184                                           "Cannot open file <filename>%1</filename> "
0185                                           "for writing.",
0186                                           tf.fileName()));
0187                 return;
0188             }
0189 
0190             // QFileDialog was run with confirmOverwrite, so we can safely
0191             // overwrite as necessary.
0192             KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(tf.fileName()), fileUrl, -1, KIO::DefaultFlags | KIO::Overwrite);
0193             KJobWidgets::setWindow(job, parent);
0194             if (!job->exec()) {
0195                 KMessageBox::error(parent, job->errorString());
0196             }
0197         }
0198     }
0199 }
0200 
0201 // Helper functions for the shutdownSaveReport
0202 class ShutdownHelper : public QObject
0203 {
0204     Q_OBJECT
0205 public:
0206     QString shutdownSaveString;
0207 
0208     void removeOldFilesIn(QDir &dir)
0209     {
0210         auto fileList = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::SortFlag::Time | QDir::Reversed);
0211         for (int i = fileList.size(); i >= 10; i--) {
0212             auto currentFile = fileList.takeFirst();
0213             dir.remove(currentFile.fileName());
0214         }
0215     }
0216 
0217     void saveReportAndQuit()
0218     {
0219         const QString dirname = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
0220         // Try to create the directory to save the logs, if we can't open the directory,
0221         // just bail out. no need to hold the shutdown process.
0222         QDir dir(dirname);
0223         if (!dir.mkpath(dirname)) {
0224             qApp->quit();
0225         }
0226 
0227         removeOldFilesIn(dir);
0228         const QString defname = dirname + QLatin1Char('/') + QStringLiteral("pid-") + QString::number(DrKonqi::pid()) + QLatin1Char('-')
0229             + getSuggestedKCrashFilename(DrKonqi::crashedApplication());
0230 
0231         QFile shutdownSaveFile(defname);
0232         if (shutdownSaveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
0233             QTextStream ts(&shutdownSaveFile);
0234             ts << shutdownSaveString;
0235             ts.flush();
0236             shutdownSaveFile.close();
0237         }
0238         deleteLater();
0239         qApp->quit();
0240     }
0241 
0242     void appendNewLine(const QString &newLine)
0243     {
0244         shutdownSaveString += newLine;
0245     }
0246 };
0247 
0248 void DrKonqi::shutdownSaveReport()
0249 {
0250     if (!DrKonqi::isEphemeralCrash()) { // No need to make a backtrace if the crash isn't ephemeral (e.g. from coredumpd)
0251         qApp->quit();
0252         return;
0253     }
0254 
0255     auto btGenerator = instance()->debuggerManager()->backtraceGenerator();
0256     auto shutdownHelper = new ShutdownHelper();
0257     QObject::connect(btGenerator, &BacktraceGenerator::done, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
0258     QObject::connect(btGenerator, &BacktraceGenerator::someError, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
0259     QObject::connect(btGenerator, &BacktraceGenerator::failedToStart, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
0260     QObject::connect(btGenerator, &BacktraceGenerator::newLine, shutdownHelper, &ShutdownHelper::appendNewLine);
0261     btGenerator->start();
0262 }
0263 
0264 void DrKonqi::setSignal(int signal)
0265 {
0266     instance()->m_signal = signal;
0267 }
0268 
0269 void DrKonqi::setAppName(const QString &appName)
0270 {
0271     instance()->m_appName = appName;
0272 }
0273 
0274 void DrKonqi::setAppPath(const QString &appPath)
0275 {
0276     instance()->m_appPath = appPath;
0277 }
0278 
0279 void DrKonqi::setAppVersion(const QString &appVersion)
0280 {
0281     instance()->m_appVersion = appVersion;
0282 }
0283 
0284 void DrKonqi::setBugAddress(const QString &bugAddress)
0285 {
0286     instance()->m_bugAddress = bugAddress;
0287 }
0288 
0289 void DrKonqi::setProgramName(const QString &programName)
0290 {
0291     instance()->m_programName = programName;
0292 }
0293 
0294 void DrKonqi::setProductName(const QString &productName)
0295 {
0296     instance()->m_productName = productName;
0297 }
0298 
0299 void DrKonqi::setPid(int pid)
0300 {
0301     instance()->m_pid = pid;
0302 }
0303 
0304 void DrKonqi::setKdeinit(bool kdeinit)
0305 {
0306     instance()->m_kdeinit = kdeinit;
0307 }
0308 
0309 void DrKonqi::setSafer(bool safer)
0310 {
0311     instance()->m_safer = safer;
0312 }
0313 
0314 void DrKonqi::setRestarted(bool restarted)
0315 {
0316     instance()->m_restarted = restarted;
0317 }
0318 
0319 void DrKonqi::setKeepRunning(bool keepRunning)
0320 {
0321     instance()->m_keepRunning = keepRunning;
0322 }
0323 
0324 void DrKonqi::setThread(int thread)
0325 {
0326     instance()->m_thread = thread;
0327 }
0328 
0329 void DrKonqi::setStartupId(const QString &startupId)
0330 {
0331     instance()->m_startupId = startupId;
0332 }
0333 
0334 int DrKonqi::signal()
0335 {
0336     return instance()->m_signal;
0337 }
0338 
0339 const QString &DrKonqi::appName()
0340 {
0341     return instance()->m_appName;
0342 }
0343 
0344 const QString &DrKonqi::appPath()
0345 {
0346     return instance()->m_appPath;
0347 }
0348 
0349 const QString &DrKonqi::appVersion()
0350 {
0351     return instance()->m_appVersion;
0352 }
0353 
0354 const QString &DrKonqi::bugAddress()
0355 {
0356     return instance()->m_bugAddress;
0357 }
0358 
0359 const QString &DrKonqi::programName()
0360 {
0361     return instance()->m_programName;
0362 }
0363 
0364 const QString &DrKonqi::productName()
0365 {
0366     return instance()->m_productName;
0367 }
0368 
0369 int DrKonqi::pid()
0370 {
0371     return instance()->m_pid;
0372 }
0373 
0374 bool DrKonqi::isKdeinit()
0375 {
0376     return instance()->m_kdeinit;
0377 }
0378 
0379 bool DrKonqi::isSafer()
0380 {
0381     return instance()->m_safer;
0382 }
0383 
0384 bool DrKonqi::isRestarted()
0385 {
0386     return instance()->m_restarted;
0387 }
0388 
0389 bool DrKonqi::isKeepRunning()
0390 {
0391     return instance()->m_keepRunning;
0392 }
0393 
0394 int DrKonqi::thread()
0395 {
0396     return instance()->m_thread;
0397 }
0398 
0399 bool DrKonqi::ignoreQuality()
0400 {
0401     static bool ignore = qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY") || qEnvironmentVariableIsSet("DRKONQI_TEST_MODE");
0402     return ignore;
0403 }
0404 
0405 bool DrKonqi::isTestingBugzilla()
0406 {
0407     return kdeBugzillaURL().contains(QLatin1String("bugstest.kde.org"));
0408 }
0409 
0410 const QString &DrKonqi::kdeBugzillaURL()
0411 {
0412     // WARNING: for practical reasons this cannot use the shared instance
0413     //   Initing the instances requires knowing the URL already, so we'd have
0414     //   an init loop. Use a local static instead. Otherwise we'd crash on
0415     //   initialization of global statics derived from our return value.
0416     //   Always copy into the local static and return that!
0417     static QString url;
0418     if (!url.isEmpty()) {
0419         return url;
0420     }
0421 
0422     url = QString::fromLocal8Bit(qgetenv("DRKONQI_KDE_BUGZILLA_URL"));
0423     if (!url.isEmpty()) {
0424         return url;
0425     }
0426 
0427     if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) {
0428         url = QStringLiteral("https://bugstest.kde.org/");
0429     } else {
0430         url = QStringLiteral("https://bugs.kde.org/");
0431     }
0432 
0433     return url;
0434 }
0435 
0436 const QString &DrKonqi::startupId()
0437 {
0438     return instance()->m_startupId;
0439 }
0440 
0441 QString DrKonqi::backendClassName()
0442 {
0443     return QString::fromLatin1(instance()->m_backend->metaObject()->className());
0444 }
0445 
0446 bool DrKonqi::isEphemeralCrash()
0447 {
0448 #ifdef SYSTEMD_AVAILABLE
0449     return qobject_cast<CoredumpBackend *>(instance()->m_backend) == nullptr; // not coredumpd backend => ephemeral
0450 #else
0451     return true;
0452 #endif
0453 }
0454 
0455 void DrKonqi::cleanupBeforeQuit()
0456 {
0457     instance()->m_backend->cleanup();
0458 }
0459 
0460 #include "drkonqi.moc"