mumble-voip_mumble/src/mumble/CrashReporter.cpp

248 lines
8.2 KiB
C++

// Copyright 2009-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include "CrashReporter.h"
#include "EnvUtils.h"
#include "NetworkConfig.h"
#include "OSInfo.h"
#include "Global.h"
#include <QtCore/QProcess>
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QHostAddress>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QPushButton>
CrashReporter::CrashReporter(QWidget *p) : QDialog(p) {
setWindowTitle(tr("Mumble Crash Report"));
QVBoxLayout *vbl = new QVBoxLayout(this);
QLabel *l;
l = new QLabel(tr("<p><b>We're terribly sorry, but it seems Mumble has crashed. Do you want to send a crash report "
"to the Mumble developers?</b></p>"
"<p>The crash report contains a partial copy of Mumble's memory at the time it crashed, and will "
"help the developers fix the problem.</p>"));
vbl->addWidget(l);
QHBoxLayout *hbl = new QHBoxLayout();
qleEmail = new QLineEdit(Global::get().s.crashReportEmail);
l = new QLabel(tr("Email address (optional)"));
l->setBuddy(qleEmail);
hbl->addWidget(l);
hbl->addWidget(qleEmail, 1);
vbl->addLayout(hbl);
qteDescription = new QTextEdit();
l->setBuddy(qteDescription);
l = new QLabel(tr("Please describe briefly, in English, what you were doing at the time of the crash"));
vbl->addWidget(l);
vbl->addWidget(qteDescription, 1);
QPushButton *pbOk = new QPushButton(tr("Send Report"));
pbOk->setDefault(true);
QPushButton *pbCancel = new QPushButton(tr("Don't send report"));
pbCancel->setAutoDefault(false);
QDialogButtonBox *dbb = new QDialogButtonBox(Qt::Horizontal);
dbb->addButton(pbOk, QDialogButtonBox::AcceptRole);
dbb->addButton(pbCancel, QDialogButtonBox::RejectRole);
connect(dbb, SIGNAL(accepted()), this, SLOT(accept()));
connect(dbb, SIGNAL(rejected()), this, SLOT(reject()));
vbl->addWidget(dbb);
qelLoop = new QEventLoop(this);
qpdProgress = nullptr;
qnrReply = nullptr;
}
CrashReporter::~CrashReporter() {
Global::get().s.crashReportEmail = qleEmail->text();
delete qnrReply;
}
void CrashReporter::uploadFinished() {
qpdProgress->reset();
QVariant varStatus = qnrReply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (!varStatus.isValid()) {
qWarning() << "CrashReporter.cpp: Invalid HTTP code attribute";
}
int httpStatusCode = varStatus.toInt();
if (httpStatusCode != 0) {
// The 2xx HTTP status codes are considered success
if (httpStatusCode >= 200 && httpStatusCode < 300) {
QMessageBox::information(nullptr, tr("Crash upload successful"),
tr("Thank you for helping make Mumble better!"));
} else {
QMessageBox::critical(nullptr, tr("Crash upload failed"),
tr("HTTP error %1: \"%2\"")
.arg(httpStatusCode)
.arg(qnrReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()));
}
} else {
QMessageBox::critical(nullptr, tr("Crash upload failed"),
tr("Internal error encountered in CrashReporter.cpp: Received network reply does not "
"contain an HTTP status code."
" Please inform a developer about error code %1")
.arg(qnrReply->error()));
}
qelLoop->exit(0);
}
void CrashReporter::uploadProgress(qint64 sent, qint64 total) {
qpdProgress->setMaximum(static_cast< int >(total));
qpdProgress->setValue(static_cast< int >(sent));
}
void CrashReporter::run() {
QByteArray qbaDumpContents;
QFile qfCrashDump(Global::get().qdBasePath.filePath(QLatin1String("mumble.dmp")));
if (!qfCrashDump.exists())
return;
qfCrashDump.open(QIODevice::ReadOnly);
#if defined(Q_OS_WIN)
/* On Windows, the .dmp file is a real minidump. */
if (qfCrashDump.peek(4) != "MDMP")
return;
qbaDumpContents = qfCrashDump.readAll();
#elif defined(Q_OS_MAC)
/*
* On OSX, the .dmp file is simply a dummy file that we
* use to find the *real* crash dump, made by the OSX
* built in crash reporter.
*/
QFileInfo qfiDump(qfCrashDump);
QDateTime qdtModification = qfiDump.lastModified();
/* Find the real crash report. */
QDir qdCrashReports(QDir::home().absolutePath() + QLatin1String("/Library/Logs/DiagnosticReports/"));
if (!qdCrashReports.exists()) {
qdCrashReports.setPath(QDir::home().absolutePath() + QLatin1String("/Library/Logs/CrashReporter/"));
}
QStringList qslFilters;
qslFilters << QString::fromLatin1("Mumble_*.crash");
qdCrashReports.setNameFilters(qslFilters);
qdCrashReports.setSorting(QDir::Time);
QFileInfoList qfilEntries = qdCrashReports.entryInfoList();
/*
* Figure out if our delta is sufficiently close to the Apple crash dump, or
* if something weird happened.
*/
foreach (QFileInfo fi, qfilEntries) {
qint64 delta = qAbs< qint64 >(qdtModification.secsTo(fi.lastModified()));
if (delta < 8) {
QFile f(fi.absoluteFilePath());
f.open(QIODevice::ReadOnly);
qbaDumpContents = f.readAll();
break;
}
}
#endif
QString details;
#ifdef Q_OS_WIN
{
QTemporaryFile qtf;
if (qtf.open()) {
qtf.close();
QProcess qp;
QStringList qsl;
qsl << QLatin1String("/t");
qsl << qtf.fileName();
QString app = QLatin1String("dxdiag.exe");
QString systemRoot = EnvUtils::getenv(QLatin1String("SystemRoot"));
if (systemRoot.count() > 0) {
app = QDir::fromNativeSeparators(systemRoot + QLatin1String("/System32/dxdiag.exe"));
}
qp.start(app, qsl);
if (qp.waitForFinished(30000)) {
if (qtf.open()) {
QByteArray qba = qtf.readAll();
details = QString::fromLocal8Bit(qba);
}
} else {
details = QLatin1String("Failed to run dxdiag");
}
qp.kill();
}
}
#endif
if (qbaDumpContents.isEmpty()) {
qWarning("CrashReporter: Empty crash dump file, not reporting.");
return;
}
if (exec() == QDialog::Accepted) {
qpdProgress = new QProgressDialog(tr("Uploading crash report"), tr("Abort upload"), 0, 100, this);
qpdProgress->setMinimumDuration(500);
qpdProgress->setValue(0);
connect(qpdProgress, SIGNAL(canceled()), qelLoop, SLOT(quit()));
QString boundary =
QString::fromLatin1("---------------------------%1").arg(QDateTime::currentDateTime().toTime_t());
QString os = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; "
"name=\"os\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2 %3\r\n")
.arg(boundary, OSInfo::getOS(), OSInfo::getOSVersion());
QString ver = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; "
"name=\"ver\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n")
.arg(boundary, Version::getRelease());
QString email = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; "
"name=\"email\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n")
.arg(boundary, qleEmail->text());
QString descr = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; "
"name=\"desc\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n")
.arg(boundary, qteDescription->toPlainText());
QString det = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; "
"name=\"details\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n%2\r\n")
.arg(boundary, details);
QString head = QString::fromLatin1("--%1\r\nContent-Disposition: form-data; name=\"dump\"; "
"filename=\"mumble.dmp\"\r\nContent-Type: binary/octet-stream\r\n\r\n")
.arg(boundary);
QString end = QString::fromLatin1("\r\n--%1--\r\n").arg(boundary);
QByteArray post = os.toUtf8() + ver.toUtf8() + email.toUtf8() + descr.toUtf8() + det.toUtf8() + head.toUtf8()
+ qbaDumpContents + end.toUtf8();
QUrl url(QLatin1String("https://crash-report.mumble.info/v1/report"));
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader,
QString::fromLatin1("multipart/form-data; boundary=%1").arg(boundary));
req.setHeader(QNetworkRequest::ContentLengthHeader, QString::number(post.size()));
Network::prepareRequest(req);
qnrReply = Global::get().nam->post(req, post);
connect(qnrReply, SIGNAL(finished()), this, SLOT(uploadFinished()));
connect(qnrReply, SIGNAL(uploadProgress(qint64, qint64)), this, SLOT(uploadProgress(qint64, qint64)));
qelLoop->exec(QEventLoop::DialogExec);
}
if (!qfCrashDump.remove())
qWarning("CrashReporeter: Unable to remove crash file.");
}