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