Use QHttpMultiPart, move the crash sender into its own file

This commit is contained in:
David Sansome 2012-11-26 20:32:14 +11:00
parent 273d3260a0
commit 39b88e1577
9 changed files with 291 additions and 240 deletions

View File

@ -185,7 +185,10 @@ if(WIN32)
option(ENABLE_WIN32_CONSOLE "Show the windows console even outside Debug mode" OFF)
endif(WIN32)
optional_component(BREAKPAD OFF "Crash reporting")
# Crash reporting requires Qt 4.8 because of the use of QHttpMultiPart
optional_component(BREAKPAD OFF "Crash reporting"
DEPENDS "Qt 4.8 or newer" "QTVERSION VERSION_GREATER 4.7.999"
)
optional_component(GOOGLE_DRIVE ON "Google Drive support"
DEPENDS "Google sparsehash" SPARSEHASH_INCLUDE_DIRS
@ -434,6 +437,8 @@ if(IMOBILEDEVICE_FOUND AND PLIST_FOUND)
endif(IMOBILEDEVICE_FOUND AND PLIST_FOUND)
if(HAVE_BREAKPAD)
set(CRASHREPORTING_HOSTNAME crashes.clementine-player.org
CACHE STRING "Server to send crash reports to")
add_subdirectory(3rdparty/google-breakpad)
endif(HAVE_BREAKPAD)

View File

@ -384,7 +384,6 @@ set(HEADERS
core/application.h
core/backgroundstreams.h
core/crashreporting.h
core/database.h
core/deletefiles.h
core/filesystemwatcherinterface.h
@ -1093,6 +1092,8 @@ optional_source(LINUX
# Breakpad source files
if(HAVE_BREAKPAD)
list(APPEND SOURCES core/crashsender.cpp)
list(APPEND HEADERS core/crashsender.h)
if(LINUX)
list(APPEND SOURCES core/crashreporting_linux.cpp)
elseif(APPLE)

View File

@ -19,6 +19,7 @@
#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}"
#define CMAKE_EXECUTABLE_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}"
#define CRASHREPORTING_HOSTNAME "${CRASHREPORTING_HOSTNAME}"
#cmakedefine ENABLE_VISUALISATIONS
#cmakedefine HAVE_AUDIOCD

View File

@ -17,195 +17,34 @@
#include "config.h"
#include "crashreporting.h"
#include "version.h"
#include "core/logging.h"
#include "crashsender.h"
#include <QApplication>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QFile>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QProgressDialog>
#include <QSysInfo>
#include <QUrl>
#include <QtDebug>
#if QT_VERSION >= 0x040800
#include <QHttpMultiPart>
#endif
const char* CrashSender::kUploadURL = "http://crashes.clementine-player.org/getuploadurl";
const char* CrashReporting::kSendCrashReportOption = "--send-crash-report";
char* CrashReporting::sPath = NULL;
bool CrashReporting::SendCrashReport(int argc, char** argv) {
if (argc != 4 || strcmp(argv[1], kSendCrashReportOption) != 0) {
#ifdef HAVE_BREAKPAD
if (argc != 3 || strcmp(argv[1], kSendCrashReportOption) != 0) {
return false;
}
QApplication a(argc, argv);
CrashSender sender(QString("%1/%2.dmp").arg(argv[2], argv[3]));
CrashSender sender(argv[2]);
if (sender.Start()) {
a.exec();
}
return true;
#else // HAVE_BREAKPAD
return false;
#endif
}
void CrashReporting::SetApplicationPath(const QString& path) {
sPath = strdup(path.toLocal8Bit().constData());
}
CrashSender::CrashSender(const QString& path)
: network_(new QNetworkAccessManager(this)),
path_(path),
file_(new QFile(path_, this)),
progress_(NULL) {
}
bool CrashSender::Start() {
if (!file_->open(QIODevice::ReadOnly)) {
qLog(Warning) << "Failed to open crash report" << path_;
return false;
}
// No tr() here.
QMessageBox prompt(QMessageBox::Warning, "Clementine has crashed!",
"Clementine has crashed! A crash report has been created and saved to "
"disk. With your permission it can be automatically sent to our server "
"so the developers can find out what happened.");
prompt.addButton("Don't send", QMessageBox::RejectRole);
prompt.addButton("Send crash report", QMessageBox::AcceptRole);
if (prompt.exec() == QDialog::Rejected) {
return false;
}
progress_ = new QProgressDialog("Uploading crash report", "Cancel", 0, 0);
progress_->show();
// We'll get a redirect first, so don't start POSTing data yet.
QNetworkReply* reply = network_->get(QNetworkRequest(QUrl(kUploadURL)));
connect(reply, SIGNAL(finished()), SLOT(RedirectFinished()));
return true;
}
void CrashSender::RedirectFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply) {
progress_->close();
return;
}
reply->deleteLater();
QUrl url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!url.isValid()) {
printf("Response didn't have a redirection target - HTTP %d\n",
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
progress_->close();
return;
}
// Get some information about the thing that crashed.
url.setQueryItems(ClientInfo());
printf("Uploading crash report to %s\n", url.toEncoded().constData());
QNetworkRequest req(url);
#if QT_VERSION >= 0x040800
QHttpPart part;
part.setHeader(QNetworkRequest::ContentDispositionHeader,
"form-data; name=\"data\", filename=\"data.dmp\"");
part.setBodyDevice(file_);
QHttpMultiPart* multi_part = new QHttpMultiPart(QHttpMultiPart::FormDataType);
multi_part->append(part);
reply = network_->post(req, multi_part);
#else
// Read the file's data
QByteArray file_data = file_->readAll();
// Find a boundary that doesn't exist in the file
QByteArray boundary;
forever {
boundary = "--------------" + QString::number(qrand(), 16).toAscii();
if (!file_data.contains(boundary)) {
break;
}
}
req.setHeader(QNetworkRequest::ContentTypeHeader,
QString("multipart/form-data; boundary=" + boundary));
// Construct the multipart/form-data
QByteArray form_data;
form_data.reserve(file_data.size() + 1024);
form_data.append("--");
form_data.append(boundary);
form_data.append("\nContent-Disposition: form-data; name=\"data\"; filename=\"data.dmp\"\n");
form_data.append("Content-Type: application/octet-stream\n\n");
form_data.append(file_data);
form_data.append("\n--");
form_data.append(boundary);
form_data.append("--");
progress_->setMaximum(form_data.size());
// Upload the data
reply = network_->post(req, form_data);
#endif
connect(reply, SIGNAL(uploadProgress(qint64,qint64)), SLOT(UploadProgress(qint64,qint64)));
connect(reply, SIGNAL(finished()), progress_, SLOT(close()));
}
void CrashSender::UploadProgress(qint64 bytes, qint64 total) {
printf("Uploaded %lld of %lld bytes\n", bytes, total);
progress_->setValue(bytes);
}
QList<QPair<QString, QString> > CrashSender::ClientInfo() const {
typedef QPair<QString, QString> Pair;
QList<Pair> ret;
ret.append(Pair("version", CLEMENTINE_VERSION_DISPLAY));
ret.append(Pair("qt_version", qVersion()));
// Hash the binary
QFile executable(QCoreApplication::applicationFilePath());
if (executable.open(QIODevice::ReadOnly)) {
QCryptographicHash hash(QCryptographicHash::Md5);
while (!executable.atEnd()) {
hash.addData(executable.read(4096));
}
ret.append(Pair("exe_md5", hash.result().toHex()));
}
// Get the OS version
#if defined(Q_OS_MAC)
ret.append(Pair("os", "mac"));
ret.append(Pair("os_version", QString::number(QSysInfo::MacintoshVersion)));
#elif defined(Q_OS_WIN)
ret.append(Pair("os", "win"));
ret.append(Pair("os_version", QString::number(QSysInfo::WindowsVersion)));
#else
ret.append(Pair("os", "linux"));
QFile lsb_release("/etc/lsb-release");
if (lsb_release.open(QIODevice::ReadOnly)) {
ret.append(Pair("os_version",
QString::fromUtf8(lsb_release.readAll()).simplified()));
}
#endif
return ret;
}

View File

@ -18,15 +18,10 @@
#ifndef CRASHREPORTING_H
#define CRASHREPORTING_H
#include <QObject>
#include <QVariantMap>
#include <QString>
#include <boost/scoped_ptr.hpp>
class QFile;
class QNetworkAccessManager;
class QProgressDialog;
namespace google_breakpad {
class ExceptionHandler;
}
@ -39,6 +34,8 @@ public:
CrashReporting();
~CrashReporting();
static const char* kSendCrashReportOption;
// If the commandline contains the --send-crash-report option, the user will
// be prompted to send the crash report and the function will return true
// (in which case the caller should exit the program). Otherwise the function
@ -49,52 +46,18 @@ public:
// --send-crash-report when a crash happens.
static void SetApplicationPath(const QString& path);
private:
// Prints the message to stdout without using libc.
static void Print(const char* message);
// Breakpad callback.
static bool Handler(const char* dump_path,
const char* minidump_id,
void* context,
bool succeeded);
static const char* application_path() { return sPath; }
private:
Q_DISABLE_COPY(CrashReporting);
static const char* kSendCrashReportOption;
static char* sPath;
boost::scoped_ptr<google_breakpad::ExceptionHandler> handler_;
};
// Asks the user if he wants to send a crash report, and displays a progress
// dialog while uploading it if he does.
class CrashSender : public QObject {
Q_OBJECT
public:
CrashSender(const QString& path);
// Returns false if the user doesn't want to send the crash report (caller
// should exit), or true if he does (caller should start the Qt event loop).
bool Start();
private slots:
void RedirectFinished();
void UploadProgress(qint64 bytes, qint64 total);
QList<QPair<QString, QString> > ClientInfo() const;
private:
static const char* kUploadURL;
QNetworkAccessManager* network_;
QString path_;
QFile* file_;
QProgressDialog* progress_;
};
#endif // CRASHREPORTING_H

View File

@ -28,11 +28,3 @@ CrashReporting::CrashReporting() {
CrashReporting::~CrashReporting() {
}
bool CrashReporting::Handler(const char* dump_path,
const char* minidump_id,
void* context,
bool succeeded) {
}
#endif // HAVE_BREAKPAD

View File

@ -24,33 +24,48 @@
#include "client/linux/handler/exception_handler.h"
#include "third_party/lss/linux_syscall_support.h"
namespace {
bool Handler(const google_breakpad::MinidumpDescriptor& dump,
void* context, bool succeeded) {
CrashReporting::Print(
"Clementine has crashed! A crash report has been saved to:\n ");
CrashReporting::Print(dump.path());
CrashReporting::Print(
"\n\nPlease send this to the developers so they can fix the problem:\n"
" http://code.google.com/p/clementine-player/issues/entry\n\n");
if (CrashReporting::application_path()) {
// We know the path to clementine, so exec it again to prompt the user to
// upload the report.
const char* argv[] = {
CrashReporting::application_path(),
CrashReporting::kSendCrashReportOption,
dump.path(),
NULL
};
sys_execv(CrashReporting::application_path(), argv);
}
return true;
}
} // namespace
CrashReporting::CrashReporting()
: handler_(new google_breakpad::ExceptionHandler(
QDir::tempPath().toLocal8Bit().constData(), NULL,
CrashReporting::Handler, this, true)) {
google_breakpad::MinidumpDescriptor(
QDir::tempPath().toLocal8Bit().constData()),
NULL, Handler, this, true, -1)) {
}
CrashReporting::~CrashReporting() {
}
bool CrashReporting::Handler(const char* dump_path,
const char* minidump_id,
void* context,
bool succeeded) {
Print("Clementine has crashed! A crash report has been saved to:\n ");
Print(dump_path);
Print("/");
Print(minidump_id);
Print("\n\nPlease send this to the developers so they can fix the problem:\n"
" http://code.google.com/p/clementine-player/issues/entry\n\n");
if (sPath) {
// We know the path to clementine, so exec it again to prompt the user to
// upload the report.
const char* argv[] = {sPath, kSendCrashReportOption, dump_path, minidump_id, NULL};
sys_execv(sPath, argv);
void CrashReporting::Print(const char* message) {
if (message) {
sys_write(1, message, strlen(message));
}
return true;
}

173
src/core/crashsender.cpp Normal file
View File

@ -0,0 +1,173 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "crashsender.h"
#include "version.h"
#include "core/logging.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QFile>
#include <QHttpMultiPart>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QProgressDialog>
#include <QSysInfo>
#include <QUrl>
const char* CrashSender::kUploadURL =
"http://" CRASHREPORTING_HOSTNAME "/getuploadurl";
CrashSender::CrashSender(const QString& path)
: network_(new QNetworkAccessManager(this)),
path_(path),
file_(new QFile(path_, this)),
progress_(NULL) {
}
bool CrashSender::Start() {
if (!file_->open(QIODevice::ReadOnly)) {
qLog(Warning) << "Failed to open crash report" << path_;
return false;
}
// No tr() here.
QMessageBox prompt(QMessageBox::Warning, "Clementine has crashed!",
"Clementine has crashed! A crash report has been created and saved to "
"disk. With your permission it can be automatically sent to our server "
"so the developers can find out what happened.");
prompt.addButton("Don't send", QMessageBox::RejectRole);
prompt.addButton("Send crash report", QMessageBox::AcceptRole);
if (prompt.exec() == QDialog::Rejected) {
return false;
}
progress_ = new QProgressDialog("Uploading crash report", "Cancel", 0, 0);
progress_->show();
// We'll get a redirect first, so don't start POSTing data yet.
QNetworkReply* reply = network_->get(QNetworkRequest(QUrl(kUploadURL)));
connect(reply, SIGNAL(finished()), SLOT(RedirectFinished()));
return true;
}
void CrashSender::RedirectFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
if (!reply) {
progress_->close();
return;
}
reply->deleteLater();
QUrl url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!url.isValid()) {
printf("Response didn't have a redirection target - HTTP %d\n",
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
progress_->close();
return;
}
printf("Uploading crash report to %s\n", url.toEncoded().constData());
QNetworkRequest req(url);
// Create the HTTP part for the crash report file
QHttpPart part;
part.setHeader(QNetworkRequest::ContentDispositionHeader,
"form-data; name=\"data\"; filename=\"data.dmp\"");
part.setBodyDevice(file_);
QHttpMultiPart* multi_part = new QHttpMultiPart(QHttpMultiPart::FormDataType);
multi_part->append(part);
// Get some information about the thing that crashed and add that to the
// request as well.
QList<ClientInfoPair> info(ClientInfo());
foreach (const ClientInfoPair& pair, info) {
QHttpPart part;
part.setHeader(QNetworkRequest::ContentDispositionHeader,
QString("form-data; name=\"" + pair.first + "\""));
part.setBody(pair.second.toUtf8());
multi_part->append(part);
}
// Start uploading the crash report
reply = network_->post(req, multi_part);
connect(reply, SIGNAL(uploadProgress(qint64,qint64)), SLOT(UploadProgress(qint64,qint64)));
connect(reply, SIGNAL(finished()), SLOT(UploadFinished()));
}
void CrashSender::UploadProgress(qint64 bytes, qint64 total) {
printf("Uploaded %lld of %lld bytes\n", bytes, total);
progress_->setValue(bytes);
}
void CrashSender::UploadFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
Q_ASSERT(reply);
reply->deleteLater();
int code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (code > 400) {
printf("Upload caused HTTP %d: %s\n", code, reply->readAll().constData());
}
progress_->close();
}
QList<CrashSender::ClientInfoPair> CrashSender::ClientInfo() const {
QList<ClientInfoPair> ret;
ret.append(ClientInfoPair("version", CLEMENTINE_VERSION_DISPLAY));
ret.append(ClientInfoPair("qt_version", qVersion()));
// Hash the binary
QFile executable(QCoreApplication::applicationFilePath());
if (executable.open(QIODevice::ReadOnly)) {
QCryptographicHash hash(QCryptographicHash::Md5);
while (!executable.atEnd()) {
hash.addData(executable.read(4096));
}
ret.append(ClientInfoPair("exe_md5", hash.result().toHex()));
}
// Get the OS version
#if defined(Q_OS_MAC)
ret.append(ClientInfoPair("os", "mac"));
ret.append(ClientInfoPair("os_version", QString::number(QSysInfo::MacintoshVersion)));
#elif defined(Q_OS_WIN)
ret.append(ClientInfoPair("os", "win"));
ret.append(ClientInfoPair("os_version", QString::number(QSysInfo::WindowsVersion)));
#else
ret.append(ClientInfoPair("os", "linux"));
QFile lsb_release("/etc/lsb-release");
if (lsb_release.open(QIODevice::ReadOnly)) {
ret.append(ClientInfoPair("os_version",
QString::fromUtf8(lsb_release.readAll()).simplified()));
}
#endif
return ret;
}

62
src/core/crashsender.h Normal file
View File

@ -0,0 +1,62 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CRASHSENDER_H
#define CRASHSENDER_H
#include <QList>
#include <QObject>
#include <QPair>
class QFile;
class QNetworkAccessManager;
class QProgressDialog;
// Asks the user if he wants to send a crash report, and displays a progress
// dialog while uploading it if he does.
class CrashSender : public QObject {
Q_OBJECT
public:
CrashSender(const QString& path);
// Returns false if the user doesn't want to send the crash report (caller
// should exit), or true if he does (caller should start the Qt event loop).
bool Start();
private:
typedef QPair<QString, QString> ClientInfoPair;
private slots:
void RedirectFinished();
void UploadProgress(qint64 bytes, qint64 total);
void UploadFinished();
QList<ClientInfoPair> ClientInfo() const;
private:
static const char* kUploadURL;
QNetworkAccessManager* network_;
QString path_;
QFile* file_;
QProgressDialog* progress_;
};
#endif // CRASHSENDER_H