Client update implementation

This commit is contained in:
Michael Milton 2015-12-28 01:58:46 +01:00
parent 36c3536e0c
commit 66fda086c3
10 changed files with 503 additions and 4 deletions

View file

@ -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} )

View file

@ -16,6 +16,7 @@ SET(cockatrice_SOURCES
src/dlg_edit_tokens.cpp
src/dlg_edit_user.cpp
src/dlg_register.cpp
src/dlg_update.cpp
src/abstractclient.cpp
src/remoteclient.cpp
src/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}
)

View file

@ -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 <QtNetwork>
#include <QProgressDialog>
#include <QDesktopServices>
#include <QMessageBox>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>
#include <QApplication>
#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);
}

View file

@ -0,0 +1,37 @@
#ifndef DLG_UPDATE_H
#define DLG_UPDATE_H
#include <QtNetwork>
#include <QProgressDialog>
#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

View file

@ -0,0 +1,121 @@
//
// Created by miguel on 28/12/15.
//
#include <algorithm>
#include <QMessageBox>
#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<QDate> 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());
}
}

View file

@ -0,0 +1,38 @@
//
// Created by miguel on 28/12/15.
//
#ifndef COCKATRICE_UPDATECHECKER_H
#define COCKATRICE_UPDATECHECKER_H
#include <QObject>
#include <QUrl>
#include <QDate>
#include <QtNetwork>
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

View file

@ -0,0 +1,66 @@
#include <QUrl>
#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);
}

View file

@ -0,0 +1,33 @@
//
// Created by miguel on 28/12/15.
//
#ifndef COCKATRICE_UPDATEDOWNLOADER_H
#define COCKATRICE_UPDATEDOWNLOADER_H
#include <QObject>
#include <QUrl>
#include <QDate>
#include <QtNetwork>
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

View file

@ -30,6 +30,7 @@
#include <QDateTime>
#include <QSystemTrayIcon>
#include <QApplication>
#include <QtNetwork>
#if QT_VERSION < 0x050000
#include <QtGui/qtextdocument.h> // 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<QMenu *> &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)

View file

@ -25,8 +25,11 @@
#include <QSystemTrayIcon>
#include <QProcess>
#include <QMessageBox>
#include <QtNetwork>
#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<QMenu *> 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();