From 66fda086c39b353708cab77e8e5607fdc506fc9e Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Mon, 28 Dec 2015 01:58:46 +0100 Subject: [PATCH] Client update implementation --- cmake/createversionfile.cmake | 6 +- cockatrice/CMakeLists.txt | 5 +- cockatrice/src/dlg_update.cpp | 179 +++++++++++++++++++++++++++ cockatrice/src/dlg_update.h | 37 ++++++ cockatrice/src/update_checker.cpp | 121 ++++++++++++++++++ cockatrice/src/update_checker.h | 38 ++++++ cockatrice/src/update_downloader.cpp | 66 ++++++++++ cockatrice/src/update_downloader.h | 33 +++++ cockatrice/src/window_main.cpp | 15 +++ cockatrice/src/window_main.h | 7 +- 10 files changed, 503 insertions(+), 4 deletions(-) create mode 100644 cockatrice/src/dlg_update.cpp create mode 100644 cockatrice/src/dlg_update.h create mode 100644 cockatrice/src/update_checker.cpp create mode 100644 cockatrice/src/update_checker.h create mode 100644 cockatrice/src/update_downloader.cpp create mode 100644 cockatrice/src/update_downloader.h diff --git a/cmake/createversionfile.cmake b/cmake/createversionfile.cmake index ca5d0d76..85c29e72 100644 --- a/cmake/createversionfile.cmake +++ b/cmake/createversionfile.cmake @@ -2,8 +2,10 @@ set(VERSION_STRING_CPP "${PROJECT_BINARY_DIR}/version_string.cpp") set(VERSION_STRING_H "${PROJECT_BINARY_DIR}/version_string.h") INCLUDE_DIRECTORIES(${PROJECT_BINARY_DIR}) -set( hstring "extern const char *VERSION_STRING\;\n" ) -set( cppstring "const char * VERSION_STRING = \"${PROJECT_VERSION_FRIENDLY}\"\;\n") +set( hstring "extern const char *VERSION_STRING\; +extern const char *VERSION_DATE\;\n" ) +set( cppstring "const char *VERSION_STRING = \"${PROJECT_VERSION_FRIENDLY}\"\; +const char *VERSION_DATE = \"${GIT_COMMIT_DATE_FRIENDLY}\"\;\n") file(WRITE ${PROJECT_BINARY_DIR}/version_string.cpp.txt ${cppstring} ) file(WRITE ${PROJECT_BINARY_DIR}/version_string.h.txt ${hstring} ) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9b951d62..03573cc6 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -16,7 +16,8 @@ SET(cockatrice_SOURCES src/dlg_edit_tokens.cpp src/dlg_edit_user.cpp src/dlg_register.cpp - src/abstractclient.cpp + src/dlg_update.cpp + src/abstractclient.cpp src/remoteclient.cpp src/main.cpp src/window_main.cpp @@ -107,6 +108,8 @@ SET(cockatrice_SOURCES src/settings/messagesettings.cpp src/settings/gamefilterssettings.cpp src/settings/layoutssettings.cpp + src/update_checker.cpp + src/update_downloader.cpp ${VERSION_STRING_CPP} ) diff --git a/cockatrice/src/dlg_update.cpp b/cockatrice/src/dlg_update.cpp new file mode 100644 index 00000000..f96ebe8d --- /dev/null +++ b/cockatrice/src/dlg_update.cpp @@ -0,0 +1,179 @@ +#define HUMAN_DOWNLOAD_URL "https://bintray.com/cockatrice/Cockatrice/Cockatrice/_latestVersion" +#define API_DOWNLOAD_BASE_URL "https://dl.bintray.com/cockatrice/Cockatrice/" +#define DATE_LENGTH 10 +#define MAX_DATE_LENGTH 100 +#define SHORT_SHA1_HASH_LENGTH 7 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dlg_update.h" +#include "window_main.h" + +DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent) { + + //Handle layout + text = new QLabel(this); + progress = new QProgressBar(this); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(this); + ok = new QPushButton("Ok", this); + manualDownload = new QPushButton("Update Anyway", this); + enableUpdateButton(false); //Unless we know there's an update available, you can't install + gotoDownload = new QPushButton("Open Download Page", this); + buttonBox->addButton(manualDownload, QDialogButtonBox::ActionRole); + buttonBox->addButton(gotoDownload, QDialogButtonBox::ActionRole); + buttonBox->addButton(ok, QDialogButtonBox::AcceptRole); + + connect(gotoDownload, SIGNAL(clicked()), this, SLOT(gotoDownloadPage())); + connect(manualDownload, SIGNAL(clicked()), this, SLOT(downloadUpdate())); + connect(ok, SIGNAL(clicked()), this, SLOT(closeDialog())); + + QVBoxLayout *parentLayout = new QVBoxLayout(this); + parentLayout->addWidget(text); + parentLayout->addWidget(progress); + parentLayout->addWidget(buttonBox); + + setLayout(parentLayout); + + //Check for SSL (this probably isn't necessary) + if (!QSslSocket::supportsSsl()) { + enableUpdateButton(false); + QMessageBox::critical( + this, + tr("Error"), + tr("Cockatrice was not built with SSL support, so cannot download updates! " + "Please visit the download page and update manually.")); + } + + //Initialize the checker and downloader class + uDownloader = new UpdateDownloader(this); + connect(uDownloader, SIGNAL(downloadSuccessful(QUrl)), this, SLOT(downloadSuccessful(QUrl))); + connect(uDownloader, SIGNAL(progressMade(qint64, qint64)), + this, SLOT(downloadProgressMade(qint64, qint64))); + connect(uDownloader, SIGNAL(error(QString)), + this, SLOT(downloadError(QString))); + + uChecker = new UpdateChecker(this); + connect(uChecker, SIGNAL(finishedCheck(bool, bool, QVariantMap * )), + this, SLOT(finishedUpdateCheck(bool, bool, QVariantMap * ))); + connect(uChecker, SIGNAL(error(QString)), + this, SLOT(updateCheckError(QString))); + + //Check for updates + beginUpdateCheck(); +} + + +void DlgUpdate::closeDialog() { + accept(); +} + + +void DlgUpdate::gotoDownloadPage() { + QUrl openUrl(HUMAN_DOWNLOAD_URL); + QDesktopServices::openUrl(openUrl); +} + +void DlgUpdate::downloadUpdate() { + setLabel("Downloading update..."); + uDownloader->beginDownload(updateUrl); +} + +void DlgUpdate::beginUpdateCheck() { + progress->setMinimum(0); + progress->setMaximum(0); + setLabel("Checking for updates..."); + uChecker->check(); +} + +void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, QVariantMap *build) { + + QString commitHash, commitDate; + + //Update the UI to say we've finished + progress->setMaximum(100); + setLabel("Finished checking for updates."); + + //If there are no available builds, then they can't auto update. + enableUpdateButton(isCompatible); + + //If there is an update, save its URL and work out its name + if (isCompatible) { + QString endUrl = (*build)["path"].toString(); + updateUrl = API_DOWNLOAD_BASE_URL + endUrl; + commitHash = (*build)["sha1"].toString().left(SHORT_SHA1_HASH_LENGTH); + commitDate = (*build)["created"].toString().remove(DATE_LENGTH, MAX_DATE_LENGTH); + } + + //Give the user the appropriate message + if (needToUpdate) { + if (isCompatible) { + + QMessageBox::StandardButton reply; + reply = QMessageBox::question(this, "Update Available", + "A new build (commit " + commitHash + ") from " + commitDate + + " is available. Download?", + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) + downloadUpdate(); + } + else { + QMessageBox::information(this, "Cockatrice Update", + tr("Your version of Cockatrice is out of date, but there are no packages" + " available for your operating system. You may have to use a developer build or build from source" + " yourself. Please visit the download page.")); + } + } + else { + //If there's no need to update, tell them that. However we still allow them to run the + //downloader themselves if there's a compatible build + QMessageBox::information(this, tr("Cockatrice Update"), tr("Your version of Cockatrice is up to date.")); + } + +} + +void DlgUpdate::enableUpdateButton(bool enable) { + manualDownload->setEnabled(enable); +} + +void DlgUpdate::setLabel(QString newText) { + text->setText(newText); +} + +void DlgUpdate::updateCheckError(QString errorString) { + setLabel("Error"); + QMessageBox::critical(this, tr("Update Error"), "An error occurred while checking for updates: " + errorString); +} + +void DlgUpdate::downloadError(QString errorString) { + setLabel("Error"); + QMessageBox::critical(this, tr("Update Error"), "An error occurred while downloading an update: " + errorString); +} + +void DlgUpdate::downloadSuccessful(QUrl filepath) { + setLabel("Installing..."); + //Try to open the installer. If it opens, quit Cockatrice + if (QDesktopServices::openUrl(filepath)) + { + QMetaObject::invokeMethod((MainWindow*) parent(), "close", Qt::QueuedConnection); + close(); + } else { + setLabel("Error"); + QMessageBox::critical(this, tr("Update Error"), "Unable to open the installer. You might be able to manually update" + " by closing Cockatrice and running the installer at " + filepath.toLocalFile() + "."); + } +} + +void DlgUpdate::downloadProgressMade(qint64 bytesRead, qint64 totalBytes) { + progress->setMaximum(totalBytes); + progress->setValue(bytesRead); +} diff --git a/cockatrice/src/dlg_update.h b/cockatrice/src/dlg_update.h new file mode 100644 index 00000000..c0b11670 --- /dev/null +++ b/cockatrice/src/dlg_update.h @@ -0,0 +1,37 @@ +#ifndef DLG_UPDATE_H +#define DLG_UPDATE_H + +#include +#include + +#include "update_checker.h" +#include "update_downloader.h" + +class DlgUpdate : public QDialog { +Q_OBJECT +public: + DlgUpdate(QWidget *parent); + +private slots: + void finishedUpdateCheck(bool needToUpdate, bool isCompatible, QVariantMap *build); + void gotoDownloadPage(); + void downloadUpdate(); + void updateCheckError(QString errorString); + void downloadSuccessful(QUrl filepath); + void downloadProgressMade(qint64 bytesRead, qint64 totalBytes); + void downloadError(QString errorString); + void closeDialog(); +private: + QUrl updateUrl; + void enableUpdateButton(bool enable); + void beginUpdateCheck(); + void setLabel(QString text); + QLabel *text; + QProgressBar *progress; + QPushButton *manualDownload, *gotoDownload, *ok; + QPushButton *cancel; + UpdateChecker *uChecker; + UpdateDownloader *uDownloader; +}; + +#endif diff --git a/cockatrice/src/update_checker.cpp b/cockatrice/src/update_checker.cpp new file mode 100644 index 00000000..52198743 --- /dev/null +++ b/cockatrice/src/update_checker.cpp @@ -0,0 +1,121 @@ +// +// Created by miguel on 28/12/15. +// + +#include +#include + +#include "update_checker.h" +#include "version_string.h" +#include "qt-json/json.h" + +#define LATEST_FILES_URL "https://api.bintray.com/packages/cockatrice/Cockatrice/Cockatrice/files" + +UpdateChecker::UpdateChecker(QObject *parent) : QObject(parent){ + //Parse the commit date. We'll use this to check for new versions + //We know the format because it's based on `git log` which is documented here: + // https://git-scm.com/docs/git-log#_commit_formatting + buildDate = QDate::fromString(VERSION_DATE, "yyyy-MM-dd"); + latestFilesUrl = QUrl(LATEST_FILES_URL); + response = NULL; + netMan = new QNetworkAccessManager(this); + build = NULL; +} + +UpdateChecker::~UpdateChecker() +{ + delete build; +} + +void UpdateChecker::check() +{ + response = netMan->get(QNetworkRequest(latestFilesUrl)); + connect(response, SIGNAL(finished()), + this, SLOT(fileListFinished())); +} + +#if defined(Q_OS_OSX) +bool UpdateChecker::downloadMatchesCurrentOS(QVariant build) +{ + return build + .toMap()["name"] + .toString() + .contains("osx"); +} +#elif defined(Q_OS_WIN) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) +bool UpdateChecker::downloadMatchesCurrentOS(QVariant build) +{ + return build + .toMap()["name"] + .toString() + .contains("qt5.exe"); +} +#else +bool UpdateChecker::downloadMatchesCurrentOS(QVariant build) +{ + return build + .toMap()["name"] + .toString() + .contains("qt4.exe"); +} +#endif +#else + +bool UpdateChecker::downloadMatchesCurrentOS(QVariant) +{ + //If the OS doesn't fit one of the above #defines, then it will never match + return false; +} + +#endif + +QDate UpdateChecker::dateFromBuild(QVariant build) +{ + QString formatString = "yyyy-MM-dd"; + QString dateString = build.toMap()["date"].toString(); + dateString = dateString.remove(formatString.length(), dateString.length()); + + return QDate::fromString(dateString, formatString); +} + +QDate UpdateChecker::findOldestBuild(QVariantList allBuilds) +{ + //Map the build array into an array of dates + std::vector dateArray(allBuilds.size()); + std::transform(allBuilds.begin(), allBuilds.end(), dateArray.begin(), dateFromBuild); + + //Return the first date + return *std::min_element(dateArray.begin(), dateArray.end()); +} + +QVariantMap *UpdateChecker::findCompatibleBuild(QVariantList allBuilds) { + + QVariantList::iterator result = std::find_if(allBuilds.begin(), allBuilds.end(), downloadMatchesCurrentOS); + + //If there is no compatible version, return NULL + if (result == allBuilds.end()) + return NULL; + else + { + QVariantMap *ret = new QVariantMap; + *ret = (*result).toMap(); + return ret; + } +} + +void UpdateChecker::fileListFinished() { + try { + QVariantList builds = QtJson::Json::parse(response->readAll()).toList(); + build = findCompatibleBuild(builds); + QDate bintrayBuildDate = findOldestBuild(builds); + + bool needToUpdate = bintrayBuildDate > buildDate; + bool compatibleVersion = build != NULL; + + emit finishedCheck(needToUpdate, compatibleVersion, build); + } + catch (const std::exception &exc){ + emit error(exc.what()); + } +} diff --git a/cockatrice/src/update_checker.h b/cockatrice/src/update_checker.h new file mode 100644 index 00000000..3ba044be --- /dev/null +++ b/cockatrice/src/update_checker.h @@ -0,0 +1,38 @@ +// +// Created by miguel on 28/12/15. +// + +#ifndef COCKATRICE_UPDATECHECKER_H +#define COCKATRICE_UPDATECHECKER_H + +#include +#include +#include +#include + +class UpdateChecker : public QObject { +Q_OBJECT +public: + UpdateChecker(QObject *parent); + ~UpdateChecker(); + void check(); +signals: + void finishedCheck(bool needToUpdate, bool isCompatible, QVariantMap *build); + void error(QString errorString); +private: + static QVariantMap *findCompatibleBuild(); + static QDate findOldestBuild(QVariantList allBuilds); + static QDate dateFromBuild(QVariant build); + static QVariantMap *findCompatibleBuild(QVariantList allBuilds); + static bool downloadMatchesCurrentOS(QVariant build); + QVariantMap *build; + QUrl latestFilesUrl; + QDate buildDate; + QNetworkAccessManager *netMan; + QNetworkReply *response; +private slots: + void fileListFinished(); +}; + + +#endif //COCKATRICE_UPDATECHECKER_H diff --git a/cockatrice/src/update_downloader.cpp b/cockatrice/src/update_downloader.cpp new file mode 100644 index 00000000..fb827a23 --- /dev/null +++ b/cockatrice/src/update_downloader.cpp @@ -0,0 +1,66 @@ +#include + +#include "update_downloader.h" + +UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent) { + netMan = new QNetworkAccessManager(this); +} + +void UpdateDownloader::beginDownload(QUrl downloadUrl) { + + //Save the original URL because we need it for the filename + if (originalUrl.isEmpty()) + originalUrl = downloadUrl; + + response = netMan->get(QNetworkRequest(downloadUrl)); + connect(response, SIGNAL(finished()), + this, SLOT(fileFinished())); + connect(response, SIGNAL(readyRead()), + this, SLOT(fileReadyRead())); + connect(response, SIGNAL(downloadProgress(qint64, qint64)), + this, SLOT(downloadProgress(qint64, qint64))); + connect(response, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(downloadError(QNetworkReply::NetworkError))); +} + +void UpdateDownloader::downloadError(QNetworkReply::NetworkError) { + emit error(response->errorString().toUtf8()); +} + +void UpdateDownloader::fileFinished() { + //If we finished but there's a redirect, follow it + QVariant redirect = response->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (!redirect.isNull()) + { + beginDownload(redirect.toUrl()); + return; + } + + //Handle any errors we had + if (response->error()) + { + emit error(response->errorString()); + return; + } + + //Work out the file name of the download + QString fileName = QDir::temp().path() + QDir::separator() + originalUrl.toString().section('/', -1); + + //Save the build in a temporary directory + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + emit error("Could not open the file for reading."); + return; + } + + file.write(response->readAll()); + file.close(); + + //Emit the success signal with a URL to the download file + emit downloadSuccessful(QUrl::fromLocalFile(fileName)); +} + +void UpdateDownloader::downloadProgress(qint64 bytesRead, qint64 totalBytes) { + emit progressMade(bytesRead, totalBytes); +} + diff --git a/cockatrice/src/update_downloader.h b/cockatrice/src/update_downloader.h new file mode 100644 index 00000000..e216f926 --- /dev/null +++ b/cockatrice/src/update_downloader.h @@ -0,0 +1,33 @@ +// +// Created by miguel on 28/12/15. +// + +#ifndef COCKATRICE_UPDATEDOWNLOADER_H +#define COCKATRICE_UPDATEDOWNLOADER_H + +#include +#include +#include +#include + +class UpdateDownloader : public QObject { +Q_OBJECT +public: + UpdateDownloader(QObject *parent); + void beginDownload(QUrl url); +signals: + void downloadSuccessful(QUrl filepath); + void progressMade(qint64 bytesRead, qint64 totalBytes); + void error(QString errorString); +private: + QUrl originalUrl; + QNetworkAccessManager *netMan; + QNetworkReply *response; +private slots: + void fileFinished(); + void downloadProgress(qint64 bytesRead, qint64 totalBytes); + void downloadError(QNetworkReply::NetworkError); +}; + + +#endif //COCKATRICE_UPDATEDOWNLOADER_H diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 91a52223..55680adf 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #if QT_VERSION < 0x050000 #include // for Qt::escape() @@ -40,6 +41,7 @@ #include "dlg_connect.h" #include "dlg_register.h" #include "dlg_settings.h" +#include "dlg_update.h" #include "tab_supervisor.h" #include "remoteclient.h" #include "localserver.h" @@ -48,6 +50,7 @@ #include "settingscache.h" #include "tab_game.h" #include "version_string.h" +#include "update_checker.h" #include "pb/game_replay.pb.h" #include "pb/room_commands.pb.h" @@ -62,6 +65,8 @@ #define GITHUB_TROUBLESHOOTING_URL "https://github.com/Cockatrice/Cockatrice/wiki/Troubleshooting" #define GITHUB_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Frequently-Asked-Questions" +#define DOWNLOAD_URL "https://dl.bintray.com/cockatrice/Cockatrice/" + const QString MainWindow::appName = "Cockatrice"; void MainWindow::updateTabMenu(const QList &newMenuList) @@ -288,6 +293,12 @@ void MainWindow::actAbout() )); } +void MainWindow::actUpdate() +{ + DlgUpdate dlg(this); + dlg.exec(); +} + void MainWindow::serverTimeout() { QMessageBox::critical(this, tr("Error"), tr("Server timeout")); @@ -495,6 +506,7 @@ void MainWindow::retranslateUi() #endif aAbout->setText(tr("&About Cockatrice")); + aUpdate->setText(tr("&Update Cockatrice")); helpMenu->setTitle(tr("&Help")); aCheckCardUpdates->setText(tr("Check for card updates...")); tabSupervisor->retranslateUi(); @@ -525,6 +537,8 @@ void MainWindow::createActions() aAbout = new QAction(this); connect(aAbout, SIGNAL(triggered()), this, SLOT(actAbout())); + aUpdate = new QAction(this); + connect(aUpdate, SIGNAL(triggered()), this, SLOT(actUpdate())); aCheckCardUpdates = new QAction(this); connect(aCheckCardUpdates, SIGNAL(triggered()), this, SLOT(actCheckCardUpdates())); @@ -566,6 +580,7 @@ void MainWindow::createMenus() helpMenu = menuBar()->addMenu(QString()); helpMenu->addAction(aAbout); + helpMenu->addAction(aUpdate); } MainWindow::MainWindow(QWidget *parent) diff --git a/cockatrice/src/window_main.h b/cockatrice/src/window_main.h index 3c7c6ed9..842eebd0 100644 --- a/cockatrice/src/window_main.h +++ b/cockatrice/src/window_main.h @@ -25,8 +25,11 @@ #include #include #include +#include + #include "abstractclient.h" #include "pb/response.pb.h" +#include "update_checker.h" class TabSupervisor; class RemoteClient; @@ -66,6 +69,7 @@ private slots: void actExit(); void actAbout(); + void actUpdate(); void iconActivated(QSystemTrayIcon::ActivationReason reason); @@ -90,7 +94,7 @@ private: QList tabMenus; QMenu *cockatriceMenu, *helpMenu; QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aFullScreen, *aSettings, *aExit, - *aAbout, *aCheckCardUpdates, *aRegister; + *aAbout, *aCheckCardUpdates, *aRegister, *aUpdate; TabSupervisor *tabSupervisor; QMenu *trayIconMenu; @@ -105,6 +109,7 @@ private: QMessageBox serverShutdownMessageBox; QProcess * cardUpdateProcess; + public: MainWindow(QWidget *parent = 0); ~MainWindow();