1 /*
2     SPDX-FileCopyrightText: 2000-2003 Hans Petter Bieker <bieker@kde.org>
3     SPDX-FileCopyrightText: 2009 George Kiagiadakis <gkiagia@users.sourceforge.net>
4     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 #include "drkonqi.h"
9 #include "drkonqi_debug.h"
10 
11 #include <chrono>
12 
13 #include <QFileDialog>
14 #include <QPointer>
15 #include <QTemporaryFile>
16 #include <QTextStream>
17 #include <QTimerEvent>
18 
19 #include <KCrash>
20 #include <KJobWidgets>
21 #include <KLocalizedString>
22 #include <KMessageBox>
23 #include <QApplication>
24 #include <kio/filecopyjob.h>
25 
26 #include "backtracegenerator.h"
27 #include "crashedapplication.h"
28 #include "debuggermanager.h"
29 #include "drkonqibackends.h"
30 #include "systeminformation.h"
31 
32 #ifdef SYSTEMD_AVAILABLE
33 #include "coredumpbackend.h"
34 #endif
35 
36 using namespace std::chrono_literals;
37 
factorizeBackend()38 static AbstractDrKonqiBackend *factorizeBackend()
39 {
40     // This is controlled by the environment because doing it as a cmdline option is supremely horrible because
41     // DrKonqi is a singleton that gets created at a random point in time, while options are only set on it afterwards.
42     // Since we don't want a nullptr backend we'll need the backend factorization to be independent of the cmdline.
43     // This could maybe be changed but would require substantial rejiggering of the singleton to not have points in
44     // time where there is no backend behind it.
45 #ifdef SYSTEMD_AVAILABLE
46     if (qgetenv("DRKONQI_BACKEND") == QByteArrayLiteral("COREDUMPD")) {
47         qunsetenv("DRKONQI_BACKEND");
48         return new CoredumpBackend;
49     }
50 #endif
51     return new KCrashBackend();
52 }
53 
DrKonqi()54 DrKonqi::DrKonqi()
55     : m_systemInformation(new SystemInformation())
56     , m_backend(factorizeBackend())
57     , m_signal(0)
58     , m_pid(0)
59     , m_kdeinit(false)
60     , m_safer(false)
61     , m_restarted(false)
62     , m_keepRunning(false)
63     , m_thread(0)
64 {
65 }
66 
~DrKonqi()67 DrKonqi::~DrKonqi()
68 {
69     delete m_systemInformation;
70     delete m_backend;
71 }
72 
73 // static
instance()74 DrKonqi *DrKonqi::instance()
75 {
76     static DrKonqi drKonqiInstance;
77     return &drKonqiInstance;
78 }
79 
80 // based on KCrashDelaySetHandler from kdeui/util/kcrash.cpp
81 class EnableCrashCatchingDelayed : public QObject
82 {
83 public:
EnableCrashCatchingDelayed()84     EnableCrashCatchingDelayed()
85     {
86         startTimer(10s);
87     }
88 
89 protected:
timerEvent(QTimerEvent * event)90     void timerEvent(QTimerEvent *event) override
91     {
92         qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching";
93         KCrash::setDrKonqiEnabled(true);
94         killTimer(event->timerId());
95         this->deleteLater();
96     }
97 };
98 
init()99 bool DrKonqi::init()
100 {
101     if (!instance()->m_backend->init()) {
102         return false;
103     } else { // all ok, continue initialization
104         // Set drkonqi to handle its own crashes, but only if the crashed app
105         // is not drkonqi already. If it is drkonqi, delay enabling crash catching
106         // to prevent recursive crashes (in case it crashes at startup)
107         if (crashedApplication()->fakeExecutableBaseName() != QLatin1String("drkonqi")) {
108             qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching";
109             KCrash::setDrKonqiEnabled(true);
110         } else {
111             new EnableCrashCatchingDelayed;
112         }
113         return true;
114     }
115 }
116 
117 // static
systemInformation()118 SystemInformation *DrKonqi::systemInformation()
119 {
120     return instance()->m_systemInformation;
121 }
122 
123 // static
debuggerManager()124 DebuggerManager *DrKonqi::debuggerManager()
125 {
126     return instance()->m_backend->debuggerManager();
127 }
128 
129 // static
crashedApplication()130 CrashedApplication *DrKonqi::crashedApplication()
131 {
132     return instance()->m_backend->crashedApplication();
133 }
134 
135 // static
saveReport(const QString & reportText,QWidget * parent)136 void DrKonqi::saveReport(const QString &reportText, QWidget *parent)
137 {
138     if (isSafer()) {
139         QTemporaryFile tf;
140         tf.setFileTemplate(QStringLiteral("XXXXXX.kcrash"));
141         tf.setAutoRemove(false);
142 
143         if (tf.open()) {
144             QTextStream textStream(&tf);
145             textStream << reportText;
146             textStream.flush();
147             KMessageBox::information(parent, xi18nc("@info", "Report saved to <filename>%1</filename>.", tf.fileName()));
148         } else {
149             KMessageBox::sorry(parent, i18nc("@info", "Could not create a file in which to save the report."));
150         }
151     } else {
152         QString defname = getSuggestedKCrashFilename(crashedApplication());
153 
154         QPointer<QFileDialog> dlg(new QFileDialog(parent, defname));
155         dlg->selectFile(defname);
156         dlg->setWindowTitle(i18nc("@title:window", "Select Filename"));
157         dlg->setAcceptMode(QFileDialog::AcceptSave);
158         dlg->setFileMode(QFileDialog::AnyFile);
159         dlg->setOption(QFileDialog::DontResolveSymlinks, false);
160         if (dlg->exec() != QDialog::Accepted) {
161             return;
162         }
163 
164         if (!dlg) {
165             // Dialog is invalid, it was probably deleted (ex. via DBus call)
166             // return and do not crash
167             return;
168         }
169 
170         QUrl fileUrl;
171         if (!dlg->selectedUrls().isEmpty())
172             fileUrl = dlg->selectedUrls().first();
173         delete dlg;
174 
175         if (fileUrl.isValid()) {
176             QTemporaryFile tf;
177             if (tf.open()) {
178                 QTextStream ts(&tf);
179                 ts << reportText;
180                 ts.flush();
181             } else {
182                 KMessageBox::sorry(parent,
183                                    xi18nc("@info",
184                                           "Cannot open file <filename>%1</filename> "
185                                           "for writing.",
186                                           tf.fileName()));
187                 return;
188             }
189 
190             // QFileDialog was run with confirmOverwrite, so we can safely
191             // overwrite as necessary.
192             KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(tf.fileName()), fileUrl, -1, KIO::DefaultFlags | KIO::Overwrite);
193             KJobWidgets::setWindow(job, parent);
194             if (!job->exec()) {
195                 KMessageBox::sorry(parent, job->errorString());
196             }
197         }
198     }
199 }
200 
201 // Helper functions for the shutdownSaveReport
202 class ShutdownHelper : public QObject
203 {
204     Q_OBJECT
205 public:
206     QString shutdownSaveString;
207 
removeOldFilesIn(QDir & dir)208     void removeOldFilesIn(QDir &dir)
209     {
210         auto fileList = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::SortFlag::Time | QDir::Reversed);
211         for (int i = fileList.size(); i >= 10; i--) {
212             auto currentFile = fileList.takeFirst();
213             dir.remove(currentFile.fileName());
214         }
215     }
216 
saveReportAndQuit()217     void saveReportAndQuit()
218     {
219         const QString dirname = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
220         // Try to create the directory to save the logs, if we can't open the directory,
221         // just bail out. no need to hold the shutdown process.
222         QDir dir(dirname);
223         if (!dir.mkpath(dirname)) {
224             qApp->quit();
225         }
226 
227         removeOldFilesIn(dir);
228         const QString defname = dirname + QLatin1Char('/') + QStringLiteral("pid-") + QString::number(DrKonqi::pid()) + QLatin1Char('-')
229             + getSuggestedKCrashFilename(DrKonqi::crashedApplication());
230 
231         QFile shutdownSaveFile(defname);
232         if (shutdownSaveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
233             QTextStream ts(&shutdownSaveFile);
234             ts << shutdownSaveString;
235             ts.flush();
236             shutdownSaveFile.close();
237         }
238         deleteLater();
239         qApp->quit();
240     }
241 
appendNewLine(const QString & newLine)242     void appendNewLine(const QString &newLine)
243     {
244         shutdownSaveString += newLine;
245     }
246 };
247 
shutdownSaveReport()248 void DrKonqi::shutdownSaveReport()
249 {
250     if (!DrKonqi::isEphemeralCrash()) { // No need to make a backtrace if the crash isn't ephemeral (e.g. from coredumpd)
251         qApp->quit();
252         return;
253     }
254 
255     auto btGenerator = instance()->debuggerManager()->backtraceGenerator();
256     auto shutdownHelper = new ShutdownHelper();
257     QObject::connect(btGenerator, &BacktraceGenerator::done, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
258     QObject::connect(btGenerator, &BacktraceGenerator::someError, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
259     QObject::connect(btGenerator, &BacktraceGenerator::failedToStart, shutdownHelper, &ShutdownHelper::saveReportAndQuit);
260     QObject::connect(btGenerator, &BacktraceGenerator::newLine, shutdownHelper, &ShutdownHelper::appendNewLine);
261     btGenerator->start();
262 }
263 
setSignal(int signal)264 void DrKonqi::setSignal(int signal)
265 {
266     instance()->m_signal = signal;
267 }
268 
setAppName(const QString & appName)269 void DrKonqi::setAppName(const QString &appName)
270 {
271     instance()->m_appName = appName;
272 }
273 
setAppPath(const QString & appPath)274 void DrKonqi::setAppPath(const QString &appPath)
275 {
276     instance()->m_appPath = appPath;
277 }
278 
setAppVersion(const QString & appVersion)279 void DrKonqi::setAppVersion(const QString &appVersion)
280 {
281     instance()->m_appVersion = appVersion;
282 }
283 
setBugAddress(const QString & bugAddress)284 void DrKonqi::setBugAddress(const QString &bugAddress)
285 {
286     instance()->m_bugAddress = bugAddress;
287 }
288 
setProgramName(const QString & programName)289 void DrKonqi::setProgramName(const QString &programName)
290 {
291     instance()->m_programName = programName;
292 }
293 
setProductName(const QString & productName)294 void DrKonqi::setProductName(const QString &productName)
295 {
296     instance()->m_productName = productName;
297 }
298 
setPid(int pid)299 void DrKonqi::setPid(int pid)
300 {
301     instance()->m_pid = pid;
302 }
303 
setKdeinit(bool kdeinit)304 void DrKonqi::setKdeinit(bool kdeinit)
305 {
306     instance()->m_kdeinit = kdeinit;
307 }
308 
setSafer(bool safer)309 void DrKonqi::setSafer(bool safer)
310 {
311     instance()->m_safer = safer;
312 }
313 
setRestarted(bool restarted)314 void DrKonqi::setRestarted(bool restarted)
315 {
316     instance()->m_restarted = restarted;
317 }
318 
setKeepRunning(bool keepRunning)319 void DrKonqi::setKeepRunning(bool keepRunning)
320 {
321     instance()->m_keepRunning = keepRunning;
322 }
323 
setThread(int thread)324 void DrKonqi::setThread(int thread)
325 {
326     instance()->m_thread = thread;
327 }
328 
setStartupId(const QString & startupId)329 void DrKonqi::setStartupId(const QString &startupId)
330 {
331     instance()->m_startupId = startupId;
332 }
333 
signal()334 int DrKonqi::signal()
335 {
336     return instance()->m_signal;
337 }
338 
appName()339 const QString &DrKonqi::appName()
340 {
341     return instance()->m_appName;
342 }
343 
appPath()344 const QString &DrKonqi::appPath()
345 {
346     return instance()->m_appPath;
347 }
348 
appVersion()349 const QString &DrKonqi::appVersion()
350 {
351     return instance()->m_appVersion;
352 }
353 
bugAddress()354 const QString &DrKonqi::bugAddress()
355 {
356     return instance()->m_bugAddress;
357 }
358 
programName()359 const QString &DrKonqi::programName()
360 {
361     return instance()->m_programName;
362 }
363 
productName()364 const QString &DrKonqi::productName()
365 {
366     return instance()->m_productName;
367 }
368 
pid()369 int DrKonqi::pid()
370 {
371     return instance()->m_pid;
372 }
373 
isKdeinit()374 bool DrKonqi::isKdeinit()
375 {
376     return instance()->m_kdeinit;
377 }
378 
isSafer()379 bool DrKonqi::isSafer()
380 {
381     return instance()->m_safer;
382 }
383 
isRestarted()384 bool DrKonqi::isRestarted()
385 {
386     return instance()->m_restarted;
387 }
388 
isKeepRunning()389 bool DrKonqi::isKeepRunning()
390 {
391     return instance()->m_keepRunning;
392 }
393 
thread()394 int DrKonqi::thread()
395 {
396     return instance()->m_thread;
397 }
398 
ignoreQuality()399 bool DrKonqi::ignoreQuality()
400 {
401     static bool ignore = qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY") || qEnvironmentVariableIsSet("DRKONQI_TEST_MODE");
402     return ignore;
403 }
404 
isTestingBugzilla()405 bool DrKonqi::isTestingBugzilla()
406 {
407     return kdeBugzillaURL().contains(QLatin1String("bugstest.kde.org"));
408 }
409 
kdeBugzillaURL()410 const QString &DrKonqi::kdeBugzillaURL()
411 {
412     // WARNING: for practical reasons this cannot use the shared instance
413     //   Initing the instances requires knowing the URL already, so we'd have
414     //   an init loop. Use a local static instead. Otherwise we'd crash on
415     //   initialization of global statics derived from our return value.
416     //   Always copy into the local static and return that!
417     static QString url;
418     if (!url.isEmpty()) {
419         return url;
420     }
421 
422     url = QString::fromLocal8Bit(qgetenv("DRKONQI_KDE_BUGZILLA_URL"));
423     if (!url.isEmpty()) {
424         return url;
425     }
426 
427     if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) {
428         url = QStringLiteral("https://bugstest.kde.org/");
429     } else {
430         url = QStringLiteral("https://bugs.kde.org/");
431     }
432 
433     return url;
434 }
435 
startupId()436 const QString &DrKonqi::startupId()
437 {
438     return instance()->m_startupId;
439 }
440 
backendClassName()441 QString DrKonqi::backendClassName()
442 {
443     return QString::fromLatin1(instance()->m_backend->metaObject()->className());
444 }
445 
isEphemeralCrash()446 bool DrKonqi::isEphemeralCrash()
447 {
448 #ifdef SYSTEMD_AVAILABLE
449     return qobject_cast<CoredumpBackend *>(instance()->m_backend) == nullptr; // not coredumpd backend => ephemeral
450 #else
451     return true;
452 #endif
453 }
454 
cleanupBeforeQuit()455 void DrKonqi::cleanupBeforeQuit()
456 {
457     instance()->m_backend->cleanup();
458 }
459 
460 #include "drkonqi.moc"
461