Files
renderdoc/qrenderdoc/Windows/Dialogs/CrashDialog.cpp
T
2022-02-17 17:38:32 +00:00

581 lines
18 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2022 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "CrashDialog.h"
#include <QApplication>
#include <QDateTime>
#include <QDesktopWidget>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QHttpMultiPart>
#include <QLabel>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QScreen>
#include <QSslSocket>
#include <QString>
#include <QUrlQuery>
#include "Code/QRDUtils.h"
#include "ui_CrashDialog.h"
const qint64 MaxUploadSize = 2250LL * 1024LL * 1024LL;
CrashDialog::CrashDialog(PersistantConfig &cfg, QVariantMap crashReportJSON, QWidget *parent)
: QDialog(parent), ui(new Ui::CrashDialog), m_Config(cfg)
{
ui->setupUi(this);
m_NetManager = new QNetworkAccessManager(this);
m_ReportPath = crashReportJSON[lit("report")].toString();
m_ReportMetadata = crashReportJSON;
const bool replayCrash = crashReportJSON[lit("replaycrash")].toUInt() != 0;
const bool forceCapture = crashReportJSON[lit("forcecapture")].toUInt() != 0;
const bool manualReport =
crashReportJSON.contains(lit("manual")) && crashReportJSON[lit("manual")].toUInt() != 0;
// remove metadata we don't send directly
m_ReportMetadata.remove(lit("report"));
m_ReportMetadata.remove(lit("forcecapture"));
m_ReportMetadata.remove(lit("replaycrash"));
setStage(ReportStage::FillingDetails);
m_CaptureFilename = m_Config.CrashReport_LastOpenedCapture;
ui->rememberEmail->setChecked(m_Config.CrashReport_ShouldRememberEmail);
ui->email->setText(m_Config.CrashReport_EmailAddress);
QFileInfo capInfo(m_CaptureFilename);
if(replayCrash && capInfo.exists())
{
// if we have a previous capture, fill out the capture group
ui->captureFilename->setTextFormat(Qt::RichText);
ui->captureFilename->setText(lit("<a href=\"file://%1\">%2</a>")
.arg(QUrl::fromLocalFile(capInfo.absoluteFilePath()).toString())
.arg(capInfo.fileName()));
// hide the preview until we have a successful thumbnail
ui->capturePreviewFrame->hide();
ICaptureFile *cap = RENDERDOC_OpenCaptureFile();
ReplayStatus status = cap->OpenFile(capInfo.absoluteFilePath(), "", NULL);
if(status == ReplayStatus::Succeeded)
{
Thumbnail thumb = cap->GetThumbnail(FileType::Raw, 320);
QImage i = QImage(thumb.data.data(), (int)thumb.width, (int)thumb.height, QImage::Format_RGB888)
.copy(0, 0, (int)thumb.width, (int)thumb.height);
if(!i.isNull())
{
ui->capturePreview->setPixmap(QPixmap::fromImage(i));
ui->capturePreview->setPreserveAspectRatio(true);
ui->capturePreviewFrame->show();
m_Thumbnail = new Thumbnail(cap->GetThumbnail(FileType::JPG, 0));
}
}
cap->Shutdown();
if(capInfo.size() > MaxUploadSize)
{
// capture is too large to upload :(
ui->captureFilename->setText(
tr("%1 is too large for upload (%2 MB).").arg(capInfo.fileName()).arg(capInfo.size() >> 20));
ui->captureUpload->setChecked(false);
ui->captureUpload->setEnabled(false);
ui->capturePreviewFrame->hide();
}
if(forceCapture)
{
ui->captureUpload->setChecked(true);
ui->captureUpload->setEnabled(false);
ui->captureUpload->setText(ui->captureUpload->text() + tr(" (required)"));
}
}
else
{
m_CaptureFilename = QString();
// otherwise hide it entirely - this is probably a crash in the injected application or
// something along those lines where a capture isn't directly associated.
ui->captureLabel->hide();
ui->captureUpload->hide();
ui->captureFilename->hide();
ui->capturePreviewFrame->hide();
}
QString text;
if(manualReport)
{
text =
tr("<p>Thank you for reporting a problem! Please take a moment to look over this "
"form to check what is being sent.</p>");
}
else if(replayCrash)
{
text =
tr("<p>RenderDoc encountered a serious problem. Please take a moment to look over this "
"form to check what has been gathered then send it off so that RenderDoc can get "
"better!</p>");
}
else
{
text =
tr("<p>A crash happened while RenderDoc was injected into your application. It's not "
"feasible to tell whether the crash was in your application or in RenderDoc's capturing "
"code. The minidump <a href=\"%1\">in the zip</a> might show the problem.</p>"
"<p>If you don't think your application crashed on its own please take a moment to "
"look over this form to check what has been gathered then send it off so that RenderDoc "
"can get better!</p>")
.arg(QUrl::fromLocalFile(m_ReportPath).toString());
}
if(m_Config.CheckUpdate_UpdateAvailable)
{
text +=
tr("<p><b><a href=\"https://renderdoc.org/builds\">An updated version of RenderDoc</a> is "
"available</b>. This bug may be fixed in a newer version, it's advised that you "
"update to see if the bug is fixed.</p>");
}
text += tr("<p>The contents of the report can be found <a href=\"%1\">in this zip</a> which "
"you can edit/censor if you wish.</p>")
.arg(QUrl::fromLocalFile(m_ReportPath).toString());
text += tr("<p>More information about <a href=\"" BUGREPORT_URL
"\">the bug "
"reporter</a> and <a href=\"" BUGREPORT_URL
"/privacy\">privacy statement</a> "
"for submissions.");
if(!QSslSocket::supportsSsl())
{
ui->send->setEnabled(false);
ui->description->setEnabled(false);
ui->captureUpload->setEnabled(false);
ui->rememberEmail->setEnabled(false);
ui->email->setEnabled(false);
text = tr(
"<p>RenderDoc encountered a serious problem. "
"Unfortunately something went wrong while initialising the bug reporter as Qt was unable "
"to load SSL support at runtime.</p>");
text +=
tr("<p>Due to legal reasons only official builds can be distributed with the OpenSSL "
"libraries needed for SSL support. "
"If you are building locally, check that ");
#if(QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
#if defined(Q_OS_WIN32)
#if QT_POINTER_SIZE == 8
text +=
tr("you have libcrypto-1_1-64.dll and libssl-1_1-64.dll available next to qrenderdoc.exe.");
#else
text += tr("you have libcrypto-1_1.dll and libssl-1_1.dll available next to qrenderdoc.exe.");
#endif
#else
text += tr("you have the runtime libopenssl library >= 1.1.1 available in your system.");
#endif
#else
#if defined(Q_OS_WIN32)
text += tr("you have libeay32.dll and ssleay32.dll available next to qrenderdoc.exe.");
#else
text += tr("you have the runtime libopenssl library >= 1.0.0 available in your system.");
#endif
#endif
text += lit("</p>");
text += tr("<p>There is no non-secure bug reporting system available so unfortunately we can't "
"proceed. If you'd like to send in the capture directly you can "
"<a href=\"mailto:baldurk@baldurk.org\">email it to me</a> attaching "
"<a href=\"%1\">this report</a> ")
.arg(QUrl::fromLocalFile(m_ReportPath).toString());
if(ui->captureFilename->isVisible())
text += tr(" and if you'd like, the capture linked below.");
text += lit("</p>");
}
ui->reportText->setTextFormat(Qt::RichText);
ui->reportText->setText(text);
setWindowFlags((windowFlags() | Qt::MSWindowsFixedSizeDialogHint) &
~Qt::WindowContextHelpButtonHint);
adjustSize();
}
CrashDialog::~CrashDialog()
{
delete m_UploadTimer;
delete m_Thumbnail;
delete ui;
}
bool CrashDialog::HasCaptureReady(PersistantConfig &cfg)
{
QFileInfo capInfo(cfg.CrashReport_LastOpenedCapture);
return capInfo.exists() && capInfo.size() <= MaxUploadSize;
}
bool CrashDialog::CaptureTooLarge(PersistantConfig &cfg)
{
QFileInfo capInfo(cfg.CrashReport_LastOpenedCapture);
return capInfo.exists() && capInfo.size() > MaxUploadSize;
}
void CrashDialog::showEvent(QShowEvent *)
{
adjustSize();
recentre();
}
void CrashDialog::resizeEvent(QResizeEvent *)
{
recentre();
}
void CrashDialog::recentre()
{
QRect scr = QApplication::primaryScreen()->geometry();
move(scr.center() - rect().center());
// when we're first shown, on this stage, move the cursor
if(m_Stage == ReportStage::FillingDetails)
QCursor::setPos(geometry().center());
}
void CrashDialog::setStage(ReportStage stage)
{
m_Stage = stage;
switch(stage)
{
case ReportStage::FillingDetails:
ui->reportGroup->show();
ui->uploadingGroup->hide();
ui->reportedGroup->hide();
break;
case ReportStage::Uploading:
ui->reportGroup->hide();
ui->uploadingGroup->show();
ui->reportedGroup->hide();
break;
case ReportStage::Reported:
ui->reportGroup->hide();
ui->uploadingGroup->hide();
ui->reportedGroup->show();
break;
}
adjustSize();
}
void CrashDialog::on_send_clicked()
{
// confirm if the user REALLY wants to upload their capture
if(ui->captureUpload->isChecked())
{
QMessageBox::StandardButton result = RDDialog::question(
this, tr("Are you sure?"), tr("Uploading your capture file will send it privately to the "
"RenderDoc server where I can "
"use it to reproduce your problem.\n\nAre you sure you are "
"OK with sending the capture "
"securely to RenderDoc's website?"));
if(result != QMessageBox::Yes)
{
// uncheck and return back so they can confirm
if(ui->captureUpload->isEnabled())
ui->captureUpload->setChecked(false);
else
RDDialog::information(
this, tr("Capture required"),
tr("<html>For unrecoverable errors like the one you encountered, without a "
"capture to reproduce the problem it's impossible to tell what "
"went wrong so a crash report is unfortunately required.\n\n"
"If you don't wish to share your capture that is OK. You can also email me at <a "
"href=\"mailto:baldurk@baldurk.org?subject=RenderDoc%20Unrecoverable%20error\">"
"baldurk@baldurk.org</a> with information and I can help investigate.</html>"));
return;
}
}
// if we haven't nagged the user before about entering their email address, do so now.
if(!m_Config.CrashReport_EmailNagged && ui->email->text().isEmpty())
{
// don't prompt about this again
m_Config.CrashReport_EmailNagged = true;
m_Config.Save();
QMessageBox::StandardButton result =
RDDialog::question(this, tr("Please consider leaving your email"),
tr("Most bug reports without an email address for contact can't be "
"resolved. Would you like to enter your email address?\n\n"
"You won't be asked about this again."));
if(result == QMessageBox::Yes)
{
// focus the email field and return so the user can enter something
ui->email->setFocus(Qt::OtherFocusReason);
return;
}
}
// save the email configuration for next time so the user can click-through.
m_Config.CrashReport_ShouldRememberEmail = ui->rememberEmail->isChecked();
if(ui->rememberEmail->isChecked() && !ui->email->text().isEmpty())
m_Config.CrashReport_EmailAddress = ui->email->text();
m_Config.Save();
sendReport();
setStage(ReportStage::Uploading);
}
void CrashDialog::sendReport()
{
delete m_Request;
m_Request = NULL;
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
// from the QHttpMultiPart example
for(QString key : m_ReportMetadata.keys())
{
QHttpPart param;
param.setHeader(QNetworkRequest::ContentDispositionHeader,
lit("form-data; name=\"%1\"").arg(key));
param.setBody(m_ReportMetadata[key].toString().toUtf8());
multiPart->append(param);
}
QString email = ui->email->text();
QString description = ui->description->toPlainText();
if(!email.isEmpty())
{
QHttpPart param;
param.setHeader(QNetworkRequest::ContentDispositionHeader, lit("form-data; name=\"email\""));
param.setBody(email.toUtf8());
multiPart->append(param);
}
if(!description.isEmpty())
{
QHttpPart param;
param.setHeader(QNetworkRequest::ContentDispositionHeader,
lit("form-data; name=\"description\""));
param.setBody(description.toUtf8());
multiPart->append(param);
}
if(!m_CaptureFilename.isEmpty() && ui->captureUpload->isChecked())
{
{
QHttpPart capture;
QFile *file = new QFile(m_CaptureFilename);
if(file->open(QIODevice::ReadOnly))
{
file->setParent(multiPart);
capture.setHeader(QNetworkRequest::ContentTypeHeader,
lit("application/x-renderdoc-capture"));
capture.setHeader(QNetworkRequest::ContentDispositionHeader,
lit("form-data; name=\"capture\"; filename=\"capture.rdc\""));
capture.setBodyDevice(file);
multiPart->append(capture);
}
}
if(m_Thumbnail)
{
QHttpPart capture;
QByteArray thumb;
thumb.insert(0, (const char *)m_Thumbnail->data.data(), m_Thumbnail->data.count());
capture.setHeader(QNetworkRequest::ContentTypeHeader, lit("image/jpeg"));
capture.setHeader(QNetworkRequest::ContentDispositionHeader,
lit("form-data; name=\"thumb\"; filename=\"thumb.jpg\""));
capture.setBody(thumb);
multiPart->append(capture);
}
}
{
QHttpPart report;
QFile *file = new QFile(m_ReportPath);
if(file->open(QIODevice::ReadOnly))
{
file->setParent(multiPart);
report.setHeader(QNetworkRequest::ContentTypeHeader, lit("application/zip"));
report.setHeader(QNetworkRequest::ContentDispositionHeader,
lit("form-data; name=\"report\"; filename=\"report.zip\""));
report.setBodyDevice(file);
multiPart->append(report);
}
else
{
ui->progressText->setText(tr("Error preparing crash report"));
// can't send report without report.zip
return;
}
}
QNetworkRequest request(QUrl(lit(BUGREPORT_URL)));
m_Request = m_NetManager->post(request, multiPart);
multiPart->setParent(m_Request);
QObject::connect(
m_Request, OverloadedSlot<QNetworkReply::NetworkError>::of(&QNetworkReply::error),
[this](QNetworkReply::NetworkError err) {
ui->progressBar->setValue(0);
ui->progressText->setText(tr("Network error uploading:\n%1").arg(m_Request->errorString()));
ui->uploadRetry->setEnabled(true);
});
ui->progressBar->setValue(0);
ui->progressText->setText(tr("Uploading report...\nCalculating time remaining"));
delete m_UploadTimer;
m_UploadTimer = new QElapsedTimer();
m_UploadTimer->start();
QObject::connect(m_Request, &QNetworkReply::uploadProgress, [this](qint64 sent, qint64 total) {
UpdateTransferProgress(sent, total, m_UploadTimer, ui->progressBar, ui->progressText,
tr("Uploading report..."));
});
QObject::connect(m_Request, &QNetworkReply::finished, [this]() {
// don't do anything if we're finished after an error
if(ui->uploadRetry->isEnabled())
return;
QString text = tr("<p>Your report has been uploaded, thank you for your help!</p>");
m_ReportID = QString::fromUtf8(m_Request->readAll());
if(!m_ReportID.isEmpty())
{
BugReport bug;
bug.reportId = m_ReportID;
QString url = bug.URL();
text +=
tr("<p>The unique anonymous URL for your report is <a href=\"%1\">%1</a>.</p>").arg(url);
}
ui->finishedText->setTextFormat(Qt::RichText);
ui->finishedText->setText(text);
setStage(ReportStage::Reported);
});
}
void CrashDialog::on_cancel_clicked()
{
// don't nag the user, just close.
reject();
}
void CrashDialog::on_uploadCancel_clicked()
{
// check that it wasn't an accident
QMessageBox::StandardButton result = RDDialog::question(
this, tr("Cancel upload?"), tr("Are you sure you want to cancel the bug report upload?"));
if(result == QMessageBox::Yes)
{
// cancel the request in flight
m_Request->abort();
delete m_Request;
// then close the window
reject();
}
}
void CrashDialog::on_uploadRetry_clicked()
{
// restart the request
sendReport();
ui->uploadRetry->setEnabled(false);
}
void CrashDialog::on_buttonBox_accepted()
{
if(!m_ReportID.isEmpty() && ui->checkUpdates->isChecked())
{
// add to list of bug reports to check for updates.
BugReport bug;
bug.reportId = m_ReportID;
bug.submitDate = QDateTime::currentDateTimeUtc();
bug.checkDate = QDateTime::currentDateTimeUtc();
m_Config.CrashReport_ReportedBugs.push_back(bug);
if(m_Config.CrashReport_ReportedBugs.count() > 20)
m_Config.CrashReport_ReportedBugs.erase(0);
m_Config.Save();
}
accept();
}
void CrashDialog::on_captureFilename_linkActivated(const QString &link)
{
if(QFileInfo::exists(m_CaptureFilename))
RevealFilenameInExternalFileBrowser(m_CaptureFilename);
}