/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2021 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 #include #include #include #include #include #include #include #include #include #include #include #include #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("%2") .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("

Thank you for reporting a problem! Please take a moment to look over this " "form to check what is being sent.

"); } else if(replayCrash) { text = tr("

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!

"); } else { text = tr("

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 in the zip might show the problem.

" "

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!

") .arg(QUrl::fromLocalFile(m_ReportPath).toString()); } if(m_Config.CheckUpdate_UpdateAvailable) { text += tr("

An updated version of RenderDoc is " "available. This bug may be fixed in a newer version, it's advised that you " "update to see if the bug is fixed.

"); } text += tr("

The contents of the report can be found in this zip which " "you can edit/censor if you wish.

") .arg(QUrl::fromLocalFile(m_ReportPath).toString()); text += tr("

More information about the bug " "reporter and privacy statement " "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( "

RenderDoc encountered a serious problem. " "Unfortunately something went wrong while initialising the bug reporter as Qt was unable " "to load SSL support at runtime.

"); text += tr("

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("

"); text += tr("

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 " "email it to me attaching " "this report ") .arg(QUrl::fromLocalFile(m_ReportPath).toString()); if(ui->captureFilename->isVisible()) text += tr(" and if you'd like, the capture linked below."); text += lit("

"); } 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("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 " "baldurk@baldurk.org with information and I can help investigate.")); 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::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("

Your report has been uploaded, thank you for your help!

"); m_ReportID = QString::fromUtf8(m_Request->readAll()); if(!m_ReportID.isEmpty()) { BugReport bug; bug.reportId = m_ReportID; QString url = bug.URL(); text += tr("

The unique anonymous URL for your report is %1.

").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); }