/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "crashhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/crashpadutils.h" #include "common/filefunctions.h" #include "version.h" namespace olive { CrashHandlerDialog::CrashHandlerDialog(const QString &report_path) { setWindowTitle(tr("Oak Video Editor")); setWindowFlags(Qt::WindowStaysOnTopHint); report_filename_ = report_path; waiting_for_upload_ = false; QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(new QLabel( tr("We're sorry, Oak Video Editor has crashed. Please help us fix it by " "sending an error report."))); QSplitter *splitter = new QSplitter(Qt::Vertical); splitter->setChildrenCollapsible(false); layout->addWidget(splitter); summary_edit_ = new QTextEdit(); summary_edit_->setPlaceholderText( tr("Describe what you were doing in as much detail as " "possible. If you can, provide steps to reproduce this crash.")); splitter->addWidget(summary_edit_); QWidget *crash_widget = new QWidget(); QVBoxLayout *crash_widget_layout = new QVBoxLayout(crash_widget); crash_widget_layout->setMargin(0); crash_widget_layout->addWidget(new QLabel(tr("Crash Report:"))); crash_report_ = new QTextEdit(); crash_report_->setReadOnly(true); crash_report_->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); crash_widget_layout->addWidget(crash_report_); splitter->addWidget(crash_widget); QHBoxLayout *btn_layout = new QHBoxLayout(); btn_layout->setMargin(0); btn_layout->addStretch(); send_report_btn_ = new QPushButton(tr("Send Error Report")); connect(send_report_btn_, &QPushButton::clicked, this, &CrashHandlerDialog::SendErrorReport); btn_layout->addWidget(send_report_btn_); dont_send_btn_ = new QPushButton(tr("Don't Send")); connect(dont_send_btn_, &QPushButton::clicked, this, &CrashHandlerDialog::reject); btn_layout->addWidget(dont_send_btn_); layout->addLayout(btn_layout); crash_report_->setEnabled(false); send_report_btn_->setEnabled(false); crash_report_->setText(tr("Waiting for crash report to be generated...")); GenerateReport(); } void CrashHandlerDialog::SetGUIObjectsEnabled(bool e) { summary_edit_->setEnabled(e); crash_report_->setEnabled(e); send_report_btn_->setEnabled(e); dont_send_btn_->setEnabled(e); } QString CrashHandlerDialog::GetSymbolPath() { QDir app_path(qApp->applicationDirPath()); QString symbols_path; #if BUILDFLAG(IS_WIN) symbols_path = app_path.filePath(QStringLiteral("symbols")); #elif BUILDFLAG(IS_LINUX) app_path.cdUp(); symbols_path = app_path.filePath(QStringLiteral("share/oak-editor/symbols")); #elif BUILDFLAG(IS_APPLE) app_path.cdUp(); symbols_path = app_path.filePath(QStringLiteral("Resources/symbols")); #endif return symbols_path; } void CrashHandlerDialog::GenerateReport() { QProcess *p = new QProcess(); connect(p, QOverload::of(&QProcess::finished), this, &CrashHandlerDialog::ReadProcessFinished); connect(p, &QProcess::readyReadStandardOutput, this, &CrashHandlerDialog::ReadProcessHasData); QString stackwalk_filename = FileFunctions::GetFormattedExecutableForPlatform( QStringLiteral("minidump_stackwalk")); QString stackwalk_bin = QDir(qApp->applicationDirPath()).filePath(stackwalk_filename); p->start(stackwalk_bin, { report_filename_, GetSymbolPath() }); crash_report_->setText( QStringLiteral("Trying to run: %1").arg(stackwalk_bin)); } void CrashHandlerDialog::ReplyFinished(QNetworkReply *reply) { waiting_for_upload_ = false; if (reply->error() == QNetworkReply::NoError) { // Close dialog QDialog::accept(); } else { QMessageBox b(this); b.setIcon(QMessageBox::Critical); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("Upload Failed")); b.setText( tr("Failed to send error report (%1). Please try again later.") .arg(QString::number(reply->error()))); b.addButton(QMessageBox::Ok); b.exec(); SetGUIObjectsEnabled(true); } } void CrashHandlerDialog::HandleSslErrors(QNetworkReply *reply, const QList &se) { QStringList errors; for (const QSslError &err : se) { errors.append(err.errorString()); } QMessageBox b(this); b.setIcon(QMessageBox::Critical); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("SSL Error")); b.setText(tr("Encountered the following SSL errors:\n\n%1") .arg(errors.join('\n'))); b.addButton(QMessageBox::Ok); b.exec(); } void CrashHandlerDialog::AttemptToFindReport() { // If we found it, use it, otherwise wait a second and try again if (report_filename_.isEmpty()) { // Couldn't find report, try again in one second QTimer::singleShot(500, this, &CrashHandlerDialog::AttemptToFindReport); } else { GenerateReport(); } } void CrashHandlerDialog::ReadProcessHasData() { report_data_.append( static_cast(sender())->readAllStandardOutput()); } void CrashHandlerDialog::ReadProcessFinished() { SetGUIObjectsEnabled(true); crash_report_->setText(report_data_); delete sender(); } void CrashHandlerDialog::SendErrorReport() { if (summary_edit_->document()->isEmpty()) { QMessageBox b(this); b.setIcon(QMessageBox::Question); b.setWindowModality(Qt::WindowModal); b.setText( tr("You must write a description to submit this crash report.")); b.addButton(QMessageBox::Ok); b.exec(); return; } QNetworkAccessManager *manager = new QNetworkAccessManager(); connect(manager, &QNetworkAccessManager::finished, this, &CrashHandlerDialog::ReplyFinished); connect(manager, &QNetworkAccessManager::sslErrors, this, &CrashHandlerDialog::HandleSslErrors); QNetworkRequest request; request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); request.setUrl( QStringLiteral("https://olivevideoeditor.org/crashpad/report.php")); // Create HTTP form QHttpMultiPart *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); // Create description section QHttpPart desc_part; desc_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); desc_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"description\"")); desc_part.setBody(summary_edit_->toPlainText().toUtf8()); multipart->append(desc_part); // Create report section QHttpPart report_part; report_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); report_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"report\"")); report_part.setBody(report_data_); multipart->append(report_part); // Create commit section QHttpPart commit_part; commit_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); commit_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"commit\"")); commit_part.setBody(kAppVersionLong.toUtf8()); multipart->append(commit_part); // Create dump section QHttpPart dump_part; dump_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); dump_part.setHeader( QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"dump\"; filename=\"%1\"") .arg(QFileInfo(report_filename_).fileName())); QFile *dump_file = new QFile(report_filename_); dump_file->open(QFile::ReadOnly); dump_part.setBodyDevice(dump_file); dump_file->setParent(multipart); // Delete file with multipart multipart->append(dump_part); // Find symbol file QDir symbol_dir(GetSymbolPath()); QString symbol_bin_name; #if BUILDFLAG(IS_WIN) symbol_bin_name = QStringLiteral("oak-editor.pdb"); #elif BUILDFLAG(IS_APPLE) symbol_bin_name = QStringLiteral("Oak"); #else symbol_bin_name = QStringLiteral("oak-editor"); #endif symbol_dir = QDir(symbol_dir.filePath(symbol_bin_name)); QStringList folders_in_symbol_path = symbol_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (folders_in_symbol_path.size() > 0) { symbol_dir = QDir(symbol_dir.filePath(folders_in_symbol_path.first())); } else { QMessageBox b(this); b.setIcon(QMessageBox::Critical); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("Failed to send report")); b.setText(tr("Failed to find symbols necessary to send report. " "This is a packaging issue. Please notify " "the maintainers of this package.")); b.addButton(QMessageBox::Ok); b.exec(); return; } // Create sym section QString symbol_filename; #if BUILDFLAG(IS_APPLE) symbol_filename = QStringLiteral("Oak.sym"); #else symbol_filename = QStringLiteral("oak-editor.sym"); #endif QString symbol_full_path = symbol_dir.filePath(symbol_filename); QHttpPart sym_part; sym_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); sym_part.setHeader( QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"sym\"; filename=\"%1\"") .arg(symbol_filename)); QFile sym_file(symbol_full_path); if (!sym_file.open(QFile::ReadOnly)) { QMessageBox b(this); b.setIcon(QMessageBox::Critical); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("Failed to send report")); b.setText(tr("Failed to open symbol file. You may not have " "permission to access it.")); b.addButton(QMessageBox::Ok); b.exec(); return; } QByteArray symbol_data = qCompress(sym_file.readAll(), 9); sym_file.close(); sym_part.setBody(symbol_data); multipart->append(sym_part); manager->post(request, multipart); SetGUIObjectsEnabled(false); waiting_for_upload_ = true; } void CrashHandlerDialog::closeEvent(QCloseEvent *e) { QMessageBox b(this); b.setIcon(QMessageBox::Warning); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("Confirm Close")); b.setText( tr("Crash report is still uploading. Closing now may result in no " "report being sent. Are you sure you wish to close?")); b.addButton(QMessageBox::Ok); b.addButton(QMessageBox::Cancel); if (waiting_for_upload_ && b.exec() == QMessageBox::Cancel) { e->ignore(); } else { e->accept(); } } } int main(int argc, char *argv[]) { QString report; #ifdef Q_OS_WINDOWS int num_args; LPWSTR *args = CommandLineToArgvW(GetCommandLineW(), &num_args); if (num_args < 2) { LocalFree(args); return 1; } report = QString::fromWCharArray(args[1]); LocalFree(args); #else if (argc < 2) { return 1; } report = argv[1]; #endif QApplication a(argc, argv); olive::CrashHandlerDialog chd(report); chd.open(); return a.exec(); }